From d310d65ded9f417708572e7cc478c29f450ce2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 12:40:22 +0000 Subject: [PATCH 01/54] Add Roborock Q10 sensor entities and snapshots --- homeassistant/components/roborock/sensor.py | 86 +++++ .../roborock/snapshots/test_sensor.ambr | 308 ++++++++++++++++++ tests/components/roborock/test_init.py | 4 +- 3 files changed, 396 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index bb0240a78da14a..7a2fc35fd0bca3 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -19,6 +19,8 @@ ZeoError, ZeoState, ) +from roborock.data.b01_q10.b01_q10_code_mappings import YXDeviceState +from roborock.devices.traits.b01.q10.status import StatusTrait as Q10StatusTrait from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol from homeassistant.components.sensor import ( @@ -34,6 +36,7 @@ from .coordinator import ( RoborockB01Q7UpdateCoordinator, + RoborockB01Q10UpdateCoordinator, RoborockConfigEntry, RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01, @@ -43,6 +46,7 @@ from .entity import ( RoborockCoordinatedEntityA01, RoborockCoordinatedEntityB01Q7, + RoborockCoordinatedEntityB01Q10, RoborockCoordinatedEntityV1, RoborockEntity, ) @@ -77,6 +81,13 @@ class RoborockSensorDescriptionB01(SensorEntityDescription): value_fn: Callable[[B01Props], StateType] +@dataclass(frozen=True, kw_only=True) +class RoborockSensorDescriptionQ10(SensorEntityDescription): + """A class that describes Roborock Q10 sensors.""" + + value_fn: Callable[[Q10StatusTrait], StateType] + + def _dock_error_value_fn(state: DeviceState) -> str | None: if ( status := state.status.dock_error_status @@ -405,6 +416,48 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: ] +Q10_B01_SENSOR_DESCRIPTIONS = [ + RoborockSensorDescriptionQ10( + key="status", + translation_key="status", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.status.value if data.status is not None else None, + entity_category=EntityCategory.DIAGNOSTIC, + options=YXDeviceState.keys(), + ), + RoborockSensorDescriptionQ10( + key="battery", + value_fn=lambda data: data.battery, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), + RoborockSensorDescriptionQ10( + key="cleaning_time", + translation_key="cleaning_time", + value_fn=lambda data: data.clean_time, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, + ), + RoborockSensorDescriptionQ10( + key="cleaning_area", + translation_key="cleaning_area", + value_fn=lambda data: data.clean_area, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfArea.SQUARE_METERS, + ), + RoborockSensorDescriptionQ10( + key="total_cleaning_count", + translation_key="total_cleaning_count", + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.clean_count, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: RoborockConfigEntry, @@ -449,6 +502,12 @@ async def async_setup_entry( for description in Q7_B01_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.data) is not None ) + entities.extend( + RoborockSensorEntityB01Q10(coordinator, description) + for coordinator in coordinators.b01_q10 + for description in Q10_B01_SENSOR_DESCRIPTIONS + if description.value_fn(coordinator.api.status) is not None + ) async_add_entities(entities) @@ -555,3 +614,30 @@ def __init__( def native_value(self) -> StateType: """Return the value reported by the sensor.""" return self.entity_description.value_fn(self.coordinator.data) + + +class RoborockSensorEntityB01Q10(RoborockCoordinatedEntityB01Q10, SensorEntity): + """Representation of a B01 Q10 Roborock sensor.""" + + entity_description: RoborockSensorDescriptionQ10 + + def __init__( + self, + coordinator: RoborockB01Q10UpdateCoordinator, + description: RoborockSensorDescriptionQ10, + ) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(f"{description.key}_{coordinator.duid_slug}", coordinator) + + async def async_added_to_hass(self) -> None: + """Register trait listener for push-based status updates.""" + await super().async_added_to_hass() + self.async_on_remove( + self.coordinator.api.status.add_update_listener(self.async_write_ha_state) + ) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.entity_description.value_fn(self.coordinator.api.status) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index d0a92bb7bfc052..b710fb7d7a9cc2 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -407,6 +407,314 @@ 'state': '3.55', }) # --- +# name: test_sensors[sensor.roborock_q10_s5_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'battery_q10_duid', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Roborock Q10 S5+ Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_cleaning_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_cleaning_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cleaning area', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cleaning area', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cleaning_area', + 'unique_id': 'cleaning_area_q10_duid', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_cleaning_area-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Cleaning area', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_cleaning_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cleaning time', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cleaning time', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cleaning_time', + 'unique_id': 'cleaning_time_q10_duid', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_cleaning_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'unknown', + 'sleepstate', + 'standbystate', + 'cleaningstate', + 'tochargestate', + 'remoteingstate', + 'chargingstate', + 'pausestate', + 'faultstate', + 'upgradestate', + 'dusting', + 'creatingmapstate', + 'mapsavestate', + 'relocationstate', + 'robotsweeping', + 'robotmoping', + 'robotsweepandmoping', + 'robottransitioning', + 'robotwaitcharge', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Status', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'status_q10_duid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock Q10 S5+ Status', + 'options': list([ + 'unknown', + 'sleepstate', + 'standbystate', + 'cleaningstate', + 'tochargestate', + 'remoteingstate', + 'chargingstate', + 'pausestate', + 'faultstate', + 'upgradestate', + 'dusting', + 'creatingmapstate', + 'mapsavestate', + 'relocationstate', + 'robotsweeping', + 'robotmoping', + 'robotsweepandmoping', + 'robottransitioning', + 'robotwaitcharge', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'chargingstate', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_total_cleaning_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_total_cleaning_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total cleaning count', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleaning count', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_cleaning_count', + 'unique_id': 'total_cleaning_count_q10_duid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_total_cleaning_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Total cleaning count', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_total_cleaning_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- # name: test_sensors[sensor.roborock_q7_filter_time_left-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index f4b766742351e3..b4cadaa9e88a26 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -566,8 +566,8 @@ async def test_zeo_device_fails_setup( "Roborock S7 2 Dock", "Dyad Pro", "Roborock Q7", + "Roborock Q10 S5+", # Zeo device is missing - # Q10 has no sensor entities } @@ -621,7 +621,7 @@ async def test_dyad_device_fails_setup( # Dyad device is missing "Zeo One", "Roborock Q7", - # Q10 has no sensor entities + "Roborock Q10 S5+", } From 2be43584ff630ba3fe9b120cf236d7323df27202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 13:52:24 +0100 Subject: [PATCH 02/54] Add additional states for Roborock vacuum in strings.json --- homeassistant/components/roborock/strings.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 64f09e5dcb65fe..60c2a415cfba16 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -414,28 +414,40 @@ "charging": "[%key:common::state::charging%]", "charging_complete": "Charging complete", "charging_problem": "Charging problem", + "chargingstate": "[%key:common::state::charging%]", "cleaning": "Cleaning", + "cleaningstate": "Cleaning", + "creatingmapstate": "Creating map", "detaching_the_mop": "Detaching the mop", "device_offline": "Device offline", "docking": "Docking", + "dusting": "Dusting", "egg_attack": "Cupid mode", "emptying_the_bin": "Emptying the bin", "error": "[%key:common::state::error%]", + "faultstate": "Fault", "going_to_target": "Going to target", "going_to_wash_the_mop": "Going to wash the mop", "idle": "[%key:common::state::idle%]", "locked": "Locked", "manual_mode": "Manual mode", "mapping": "Mapping", + "mapsavestate": "Saving map", "paused": "[%key:common::state::paused%]", + "pausestate": "[%key:common::state::paused%]", "remote_control_active": "Remote control active", + "remoteingstate": "Remote control", "returning_home": "Returning home", "segment_cleaning": "Segment cleaning", "shutting_down": "Shutting down", + "sleepstate": "Sleeping", "spot_cleaning": "Spot cleaning", + "standbystate": "Standby", "starting": "Starting", + "tochargestate": "Going to charge", "unknown": "Unknown", "updating": "Updating", + "upgradestate": "Upgrading", "washing_the_mop": "Washing the mop", "zoned_cleaning": "Zoned cleaning" } From ea847abf68c03fb11f8c9c3a95874c9eca3ed0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 16:00:16 +0100 Subject: [PATCH 03/54] Add new states for Roborock vacuum in strings.json --- homeassistant/components/roborock/strings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 60c2a415cfba16..d0147e0177ea66 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -435,9 +435,15 @@ "mapsavestate": "Saving map", "paused": "[%key:common::state::paused%]", "pausestate": "[%key:common::state::paused%]", + "relocationstate": "Relocating", "remote_control_active": "Remote control active", "remoteingstate": "Remote control", "returning_home": "Returning home", + "robotmoping": "Mopping", + "robotsweepandmoping": "Sweep and mop", + "robotsweeping": "Sweeping", + "robottransitioning": "Transitioning", + "robotwaitcharge": "Waiting to charge", "segment_cleaning": "Segment cleaning", "shutting_down": "Shutting down", "sleepstate": "Sleeping", From 21c06511f7055db7a581bc4601ed918d1c294c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 16:42:19 +0100 Subject: [PATCH 04/54] feat(roborock): add consumable life and total stats sensors for Q10 Add new sensor entities for Roborock Q10 devices: - main_brush_life, side_brush_life, filter_life, sensor_life (remaining %) - total_cleaning_area, total_cleaning_time, total_cleaning_count - clean_percent (current cleaning progress) Also wait for the first MQTT push during coordinator setup so that sensor state is populated immediately after async_config_entry_first_refresh, consistent with V1/Q7 coordinator behavior. A 10s timeout handles the offline case gracefully. Update conftest mock to trigger status listeners on refresh() so tests resolve instantly without timeout. --- .../components/roborock/coordinator.py | 28 +- homeassistant/components/roborock/sensor.py | 61 ++- .../components/roborock/strings.json | 12 + tests/components/roborock/conftest.py | 4 +- .../roborock/snapshots/test_sensor.ambr | 372 +++++++++++++++++- 5 files changed, 470 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index e20670706c96e7..f52ee824d91cb3 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -603,15 +604,36 @@ def __init__( self._device = device self.api = api self.device_info = get_device_info(device) + self._first_refresh_done = False async def _async_update_data(self) -> None: """Request a status push from the device. - This sends a fire-and-forget REQUEST_DPS command. The actual data - update will arrive asynchronously via the push listener. + On the first call, waits for the device to respond so that status + fields are populated before async_setup_entry proceeds. Subsequent + calls remain fire-and-forget because entities receive updates through + their own push listeners. """ try: - await self.api.refresh() + if not self._first_refresh_done: + # Wait for the first push response so that sensor value_fn + # filters work correctly at setup time (same behaviour as V1/Q7 + # coordinators which fetch data synchronously). + received = asyncio.Event() + unsubscribe = self.api.status.add_update_listener(received.set) + try: + await self.api.refresh() + async with asyncio.timeout(10): + await received.wait() + except TimeoutError: + _LOGGER.debug( + "Q10 device did not respond in time during first refresh" + ) + finally: + unsubscribe() + self._first_refresh_done = True + else: + await self.api.refresh() except RoborockException as ex: _LOGGER.debug("Failed to request Q10 data: %s", ex) raise UpdateFailed( diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 7a2fc35fd0bca3..057bf77a447a2a 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -452,9 +452,62 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: key="total_cleaning_count", translation_key="total_cleaning_count", state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda data: data.clean_count, + value_fn=lambda data: data.total_clean_count, entity_category=EntityCategory.DIAGNOSTIC, ), + RoborockSensorDescriptionQ10( + key="total_cleaning_area", + translation_key="total_cleaning_area", + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_clean_area, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfArea.SQUARE_METERS, + ), + RoborockSensorDescriptionQ10( + key="total_cleaning_time", + translation_key="total_cleaning_time", + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_clean_time, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, + ), + RoborockSensorDescriptionQ10( + key="main_brush_life", + translation_key="main_brush_life", + value_fn=lambda data: data.main_brush_life, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), + RoborockSensorDescriptionQ10( + key="side_brush_life", + translation_key="side_brush_life", + value_fn=lambda data: data.side_brush_life, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), + RoborockSensorDescriptionQ10( + key="filter_life", + translation_key="filter_life", + value_fn=lambda data: data.filter_life, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), + RoborockSensorDescriptionQ10( + key="sensor_life", + translation_key="sensor_life", + value_fn=lambda data: data.sensor_life, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), + RoborockSensorDescriptionQ10( + key="clean_percent", + translation_key="clean_percent", + value_fn=lambda data: data.cleaning_progress, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), ] @@ -502,11 +555,15 @@ async def async_setup_entry( for description in Q7_B01_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.data) is not None ) + # The Q10 coordinator waits for the first MQTT push during its initial + # refresh, so status fields are populated by setup time. However, a timeout + # (e.g. device offline) could leave them as None, so we skip the filter and + # register all sensors unconditionally — the entity will report unavailable + # until the device responds. entities.extend( RoborockSensorEntityB01Q10(coordinator, description) for coordinator in coordinators.b01_q10 for description in Q10_B01_SENSOR_DESCRIPTIONS - if description.value_fn(coordinator.api.status) is not None ) async_add_entities(entities) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index d0147e0177ea66..db207bf3819ea7 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -365,6 +365,9 @@ "water_empty": "Water empty" } }, + "filter_life": { + "name": "Filter remaining life" + }, "filter_time_left": { "name": "Filter time left" }, @@ -374,6 +377,9 @@ "last_clean_start": { "name": "Last clean begin" }, + "main_brush_life": { + "name": "Main brush remaining life" + }, "main_brush_time_left": { "name": "Main brush time left" }, @@ -399,9 +405,15 @@ "waiting_for_orders": "Waiting for orders" } }, + "sensor_life": { + "name": "Sensor remaining life" + }, "sensor_time_left": { "name": "Sensor time left" }, + "side_brush_life": { + "name": "Side brush remaining life" + }, "side_brush_time_left": { "name": "Side brush time left" }, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d574d495f8a2da..9a39ca049f410f 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -182,7 +182,9 @@ def create_b01_q10_trait() -> Mock: q10_trait.vacuum = AsyncMock() q10_trait.command = AsyncMock() - q10_trait.refresh = AsyncMock() + # Simulate a device push: notify status listeners when refresh is called, + # so the coordinator's first-refresh wait resolves immediately in tests. + q10_trait.refresh = AsyncMock(side_effect=status._notify_update) return q10_trait diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index b710fb7d7a9cc2..feaf9d796a032e 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -510,6 +510,57 @@ 'state': '15', }) # --- +# name: test_sensors[sensor.roborock_q10_s5_cleaning_progress-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_cleaning_progress', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cleaning progress', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cleaning progress', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'clean_percent', + 'unique_id': 'clean_percent_q10_duid', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_cleaning_progress-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Cleaning progress', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_cleaning_progress', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.roborock_q10_s5_cleaning_time-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -568,6 +619,210 @@ 'state': '2.0', }) # --- +# name: test_sensors[sensor.roborock_q10_s5_filter_remaining_life-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_filter_remaining_life', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Filter remaining life', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter remaining life', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_life', + 'unique_id': 'filter_life_q10_duid', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_filter_remaining_life-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Filter remaining life', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_filter_remaining_life', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_main_brush_remaining_life-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_main_brush_remaining_life', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main brush remaining life', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Main brush remaining life', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_brush_life', + 'unique_id': 'main_brush_life_q10_duid', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_main_brush_remaining_life-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Main brush remaining life', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_main_brush_remaining_life', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_sensor_remaining_life-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_sensor_remaining_life', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Sensor remaining life', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensor remaining life', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sensor_life', + 'unique_id': 'sensor_life_q10_duid', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_sensor_remaining_life-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Sensor remaining life', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_sensor_remaining_life', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_side_brush_remaining_life-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_side_brush_remaining_life', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Side brush remaining life', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Side brush remaining life', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'side_brush_life', + 'unique_id': 'side_brush_life_q10_duid', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_side_brush_remaining_life-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Side brush remaining life', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_side_brush_remaining_life', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.roborock_q10_s5_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -662,6 +917,60 @@ 'state': 'chargingstate', }) # --- +# name: test_sensors[sensor.roborock_q10_s5_total_cleaning_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_total_cleaning_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total cleaning area', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleaning area', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_cleaning_area', + 'unique_id': 'total_cleaning_area_q10_duid', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_total_cleaning_area-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Total cleaning area', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_total_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.roborock_q10_s5_total_cleaning_count-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -712,7 +1021,68 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_total_cleaning_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_total_cleaning_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total cleaning time', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning time', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_cleaning_time', + 'unique_id': 'total_cleaning_time_q10_duid', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_total_cleaning_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Total cleaning time', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_total_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.roborock_q7_filter_time_left-entry] From b7caf32268248cbd5d100cd7dbb645314eb31206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 20:09:30 +0100 Subject: [PATCH 05/54] Add missing icons for roborock sensors Adds icons for the following entities: - battery: Battery level indicator - cleaning_time: Current cleaning time - mop_clean_remaining: Remaining mop drying time - brush_remaining: Remaining brush time (DYAD) - main_brush_life: Main brush life percentage (Q10) - side_brush_life: Side brush life percentage (Q10) - filter_life: Filter life percentage (Q10) - sensor_life: Sensor life percentage (Q10) - mop_life_time_left: Mop lifetime remaining (B01) - times_after_clean: Total cleaning count (ZEO) - q7_status: Q7 device status (B01) --- homeassistant/components/roborock/icons.json | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index f6053090bb7a97..414185b825b629 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -40,6 +40,12 @@ } }, "sensor": { + "battery": { + "default": "mdi:battery" + }, + "brush_remaining": { + "default": "mdi:brush" + }, "clean_percent": { "default": "mdi:progress-check" }, @@ -49,12 +55,18 @@ "cleaning_brush_time_left": { "default": "mdi:brush" }, + "cleaning_time": { + "default": "mdi:clock-outline" + }, "countdown": { "default": "mdi:clock-outline" }, "dock_error": { "default": "mdi:garage-open" }, + "filter_life": { + "default": "mdi:air-filter" + }, "filter_time_left": { "default": "mdi:air-filter" }, @@ -64,12 +76,30 @@ "last_clean_start": { "default": "mdi:clock-time-twelve" }, + "main_brush_life": { + "default": "mdi:brush" + }, "main_brush_time_left": { "default": "mdi:brush" }, + "mop_clean_remaining": { + "default": "mdi:clock-outline" + }, + "mop_life_time_left": { + "default": "mdi:texture" + }, + "q7_status": { + "default": "mdi:information-outline" + }, + "sensor_life": { + "default": "mdi:eye-outline" + }, "sensor_time_left": { "default": "mdi:eye-outline" }, + "side_brush_life": { + "default": "mdi:brush" + }, "side_brush_time_left": { "default": "mdi:brush" }, @@ -79,6 +109,9 @@ "strainer_time_left": { "default": "mdi:filter-variant" }, + "times_after_clean": { + "default": "mdi:counter" + }, "total_cleaning_area": { "default": "mdi:texture-box" }, From 95b273c8ff7ef6316c9c08854407d9d331546b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 20:28:48 +0100 Subject: [PATCH 06/54] Adds eye icon for the sensor remaining life entity --- homeassistant/components/roborock/icons.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 414185b825b629..c86ed602af9336 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -94,6 +94,9 @@ "sensor_life": { "default": "mdi:eye-outline" }, + "sensor_remaining_life": { + "default": "mdi:eye-outline" + }, "sensor_time_left": { "default": "mdi:eye-outline" }, From 5f42242b0d728f953b052aff94039bbadb756c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 20:35:04 +0100 Subject: [PATCH 07/54] Refine Q10 coordinator setup behavior Stop waiting for an arbitrary first MQTT status message in the Q10 coordinator initial refresh path, and keep Q10 sensors registered unconditionally with push-based updates as values arrive. --- .../components/roborock/coordinator.py | 29 +++---------------- homeassistant/components/roborock/sensor.py | 8 ++--- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index f52ee824d91cb3..0605b19b499b95 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -604,36 +603,16 @@ def __init__( self._device = device self.api = api self.device_info = get_device_info(device) - self._first_refresh_done = False async def _async_update_data(self) -> None: """Request a status push from the device. - On the first call, waits for the device to respond so that status - fields are populated before async_setup_entry proceeds. Subsequent - calls remain fire-and-forget because entities receive updates through - their own push listeners. + This coordinator does not wait for any specific MQTT payload because + push messages are asynchronous and not guaranteed to contain every + field. Entities subscribe to trait updates and update as values arrive. """ try: - if not self._first_refresh_done: - # Wait for the first push response so that sensor value_fn - # filters work correctly at setup time (same behaviour as V1/Q7 - # coordinators which fetch data synchronously). - received = asyncio.Event() - unsubscribe = self.api.status.add_update_listener(received.set) - try: - await self.api.refresh() - async with asyncio.timeout(10): - await received.wait() - except TimeoutError: - _LOGGER.debug( - "Q10 device did not respond in time during first refresh" - ) - finally: - unsubscribe() - self._first_refresh_done = True - else: - await self.api.refresh() + await self.api.refresh() except RoborockException as ex: _LOGGER.debug("Failed to request Q10 data: %s", ex) raise UpdateFailed( diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 057bf77a447a2a..c9e346358dedf1 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -555,11 +555,9 @@ async def async_setup_entry( for description in Q7_B01_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.data) is not None ) - # The Q10 coordinator waits for the first MQTT push during its initial - # refresh, so status fields are populated by setup time. However, a timeout - # (e.g. device offline) could leave them as None, so we skip the filter and - # register all sensors unconditionally — the entity will report unavailable - # until the device responds. + # Q10 status is push-based and values may arrive asynchronously after setup. + # Register all sensors unconditionally and let each entity become available + # as trait updates are received. entities.extend( RoborockSensorEntityB01Q10(coordinator, description) for coordinator in coordinators.b01_q10 From 8c6ceb6e626183d6cd6bf5667cafe848dc571627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 22:26:11 +0100 Subject: [PATCH 08/54] Remove sensor_remaining_life icon entry from icons.json --- homeassistant/components/roborock/icons.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index c86ed602af9336..414185b825b629 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -94,9 +94,6 @@ "sensor_life": { "default": "mdi:eye-outline" }, - "sensor_remaining_life": { - "default": "mdi:eye-outline" - }, "sensor_time_left": { "default": "mdi:eye-outline" }, From dd6bd9de20a0935ec9f8bbd6c170d73b6b23fdad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 22:27:00 +0100 Subject: [PATCH 09/54] Refactor comment for device push simulation in Q10 trait tests --- tests/components/roborock/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 9a39ca049f410f..2f37d84adccdae 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -182,8 +182,8 @@ def create_b01_q10_trait() -> Mock: q10_trait.vacuum = AsyncMock() q10_trait.command = AsyncMock() - # Simulate a device push: notify status listeners when refresh is called, - # so the coordinator's first-refresh wait resolves immediately in tests. + # Simulate a device push in tests: notify status listeners whenever + # refresh() is called so they see updated state immediately. q10_trait.refresh = AsyncMock(side_effect=status._notify_update) return q10_trait From 90aa85c3a165bea823026ad304a36b9eb96e6057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 22:28:08 +0100 Subject: [PATCH 10/54] Update comment for sensor registration in Q10 setup to clarify state updates --- homeassistant/components/roborock/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index c9e346358dedf1..1b9dbf9eaefb6b 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -556,7 +556,7 @@ async def async_setup_entry( if description.value_fn(coordinator.data) is not None ) # Q10 status is push-based and values may arrive asynchronously after setup. - # Register all sensors unconditionally and let each entity become available + # Register all sensors unconditionally and let each entity's state be updated # as trait updates are received. entities.extend( RoborockSensorEntityB01Q10(coordinator, description) From 1617ef0deca11af5d131565281c749b460eef521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 22:48:07 +0100 Subject: [PATCH 11/54] Align roborock icon key with mop drying translation key --- homeassistant/components/roborock/icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 414185b825b629..bb1cacb6e2dcf7 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -82,7 +82,7 @@ "main_brush_time_left": { "default": "mdi:brush" }, - "mop_clean_remaining": { + "mop_drying_remaining_time": { "default": "mdi:clock-outline" }, "mop_life_time_left": { From 68f2bc8012e3719a2f23241ed9f96d53b0a21d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 23:38:51 +0100 Subject: [PATCH 12/54] Remove redundant comments regarding Q10 sensor registration process --- homeassistant/components/roborock/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 1b9dbf9eaefb6b..afaab856eff304 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -555,9 +555,6 @@ async def async_setup_entry( for description in Q7_B01_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.data) is not None ) - # Q10 status is push-based and values may arrive asynchronously after setup. - # Register all sensors unconditionally and let each entity's state be updated - # as trait updates are received. entities.extend( RoborockSensorEntityB01Q10(coordinator, description) for coordinator in coordinators.b01_q10 From 0ede1fecd03184c08b03980b2e17d2899d1f270e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 20:15:42 +0100 Subject: [PATCH 13/54] Adds eye icon for the sensor remaining life entity --- homeassistant/components/roborock/icons.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index bb1cacb6e2dcf7..e826f7d21d3730 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -94,6 +94,9 @@ "sensor_life": { "default": "mdi:eye-outline" }, + "sensor_remaining_life": { + "default": "mdi:eye-outline" + }, "sensor_time_left": { "default": "mdi:eye-outline" }, From 097567026abfe2a29c77742c1426e440674493ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 20:20:10 +0100 Subject: [PATCH 14/54] Fix Q10 vacuum_error sensor support Add vacuum_error sensor for Q10 B01 devices using the fault value and make value extraction compatible with python-roborock versions that do not yet expose the fault attribute. --- homeassistant/components/roborock/sensor.py | 6 +++ .../roborock/snapshots/test_sensor.ambr | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index afaab856eff304..2f3954edf94bbb 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -508,6 +508,12 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ), + RoborockSensorDescriptionQ10( + key="vacuum_error", + translation_key="vacuum_error", + value_fn=lambda data: getattr(data, "fault", None), + entity_category=EntityCategory.DIAGNOSTIC, + ), ] diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index feaf9d796a032e..32d7f21f2588c5 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -1085,6 +1085,56 @@ 'state': 'unknown', }) # --- +# name: test_sensors[sensor.roborock_q10_s5_vacuum_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_vacuum_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Vacuum error', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vacuum error', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'vacuum_error', + 'unique_id': 'vacuum_error_q10_duid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_vacuum_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock Q10 S5+ Vacuum error', + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_vacuum_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.roborock_q7_filter_time_left-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ From 39b1ca06192ff38b4cff7ac2dd10aa103bc9f896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 21 Mar 2026 23:57:54 +0100 Subject: [PATCH 15/54] Add test for Q10 vacuum error sensor updates from push notifications --- tests/components/roborock/test_sensor.py | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 7b14fec6204215..f48d394c39d52d 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -3,10 +3,12 @@ import pytest from syrupy.assertion import SnapshotAssertion -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 +from .conftest import FakeDevice + from tests.common import MockConfigEntry, snapshot_platform @@ -24,3 +26,25 @@ async def test_sensors( ) -> None: """Test sensors and check test values are correctly set.""" await snapshot_platform(hass, entity_registry, snapshot, setup_entry.entry_id) + + +async def test_q10_vacuum_error_updates_from_push( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + fake_q10_vacuum: FakeDevice, +) -> None: + """Test Q10 vacuum error sensor updates when status trait pushes updates.""" + entity_id = "sensor.roborock_q10_s5_vacuum_error" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNKNOWN + + assert fake_q10_vacuum.b01_q10_properties is not None + fake_q10_vacuum.b01_q10_properties.status.fault = "main_brush_jammed" + + await fake_q10_vacuum.b01_q10_properties.refresh() + await hass.async_block_till_done() + + updated_state = hass.states.get(entity_id) + assert updated_state is not None + assert updated_state.state == "main_brush_jammed" From a45089832234df0c3ef2dd20b76341bbe51c18d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 00:15:24 +0100 Subject: [PATCH 16/54] Implement Q10 vacuum refresh simulation using DPS update API --- tests/components/roborock/conftest.py | 28 ++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 2f37d84adccdae..ead00658378ca0 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -33,6 +33,7 @@ ZeoError, ZeoState, ) +from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP from roborock.devices.device import RoborockDevice from roborock.devices.device_manager import DeviceManager from roborock.devices.traits.b01.q10.status import StatusTrait as Q10StatusTrait @@ -182,9 +183,30 @@ def create_b01_q10_trait() -> Mock: q10_trait.vacuum = AsyncMock() q10_trait.command = AsyncMock() - # Simulate a device push in tests: notify status listeners whenever - # refresh() is called so they see updated state immediately. - q10_trait.refresh = AsyncMock(side_effect=status._notify_update) + + def _raw_value(value: Any) -> Any: + return value.value if hasattr(value, "value") else value + + async def refresh_side_effect() -> None: + """Simulate a device push via the public Q10 DPS update API.""" + dps: dict[B01_Q10_DP, Any] = { + B01_Q10_DP.STATUS: _raw_value(status.status), + B01_Q10_DP.BATTERY: status.battery, + B01_Q10_DP.FAN_LEVEL: _raw_value(status.fan_level), + B01_Q10_DP.WATER_LEVEL: _raw_value(status.water_level), + B01_Q10_DP.CLEAN_COUNT: status.clean_count, + B01_Q10_DP.CLEAN_TIME: status.clean_time, + B01_Q10_DP.CLEAN_AREA: status.clean_area, + } + if hasattr(status, "child_lock"): + dps[B01_Q10_DP.CHILD_LOCK] = _raw_value(status.child_lock) + if hasattr(status, "not_disturb"): + dps[B01_Q10_DP.NOT_DISTURB] = _raw_value(status.not_disturb) + if hasattr(status, "dust_switch"): + dps[B01_Q10_DP.DUST_SWITCH] = _raw_value(status.dust_switch) + status.update_from_dps(dps) + + q10_trait.refresh = AsyncMock(side_effect=refresh_side_effect) return q10_trait From dbf2d5d5a6a0c1f097fc2822fbde05629fbfd03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 00:29:20 +0100 Subject: [PATCH 17/54] Add Q10 vacuum error handling and update error messages --- homeassistant/components/roborock/sensor.py | 13 +- .../components/roborock/strings.json | 8 ++ .../roborock/snapshots/test_sensor.ambr | 116 +++++++++++++++++- tests/components/roborock/test_sensor.py | 2 +- 4 files changed, 135 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 2f3954edf94bbb..d14ae8a335beb6 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -88,6 +88,15 @@ class RoborockSensorDescriptionQ10(SensorEntityDescription): value_fn: Callable[[Q10StatusTrait], StateType] +def _q10_error_value_fn(status: Q10StatusTrait) -> str | None: + fault = getattr(status, "fault", None) + if fault is None: + return None + if isinstance(fault, str): + return fault + return RoborockErrorCode(fault).name + + def _dock_error_value_fn(state: DeviceState) -> str | None: if ( status := state.status.dock_error_status @@ -511,7 +520,9 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: RoborockSensorDescriptionQ10( key="vacuum_error", translation_key="vacuum_error", - value_fn=lambda data: getattr(data, "fault", None), + value_fn=_q10_error_value_fn, + device_class=SensorDeviceClass.ENUM, + options=RoborockErrorCode.keys(), entity_category=EntityCategory.DIAGNOSTIC, ), ] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index db207bf3819ea7..a932b9ddcc73aa 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -488,10 +488,14 @@ "vacuum_error": { "name": "Vacuum error", "state": { + "audio_error": "Audio error", "battery_error": "Battery error", "bumper_stuck": "Bumper stuck", "cannot_cross_carpet": "Cannot cross carpet", "charging_error": "Charging error", + "check_clean_carouse": "Check the cleaning carousel", + "clean_carousel_exception": "Cleaning carousel error", + "clean_carousel_water_full": "Cleaning carousel water full", "clear_brush_exception": "Check that the water filter has been correctly installed", "clear_brush_exception_2": "Positioning button error", "clear_water_box_exception": "Clean water tank empty", @@ -503,6 +507,7 @@ "dirty_water_box_hoare": "Check the dirty water tank", "dock": "Dock not connected to power", "dock_locator_error": "Dock locator error", + "drain_water_exception": "Drain water exception", "fan_error": "Fan error", "filter_blocked": "Filter blocked", "filter_screen_exception": "Clean the dock water filter", @@ -518,6 +523,7 @@ "no_dustbin": "No dustbin", "nogo_zone_detected": "No-go zone detected", "none": "None", + "optical_flow_sensor_dirt": "Optical flow sensor dirty", "return_to_dock_fail": "Return to dock fail", "robot_on_carpet": "Robot on carpet", "robot_tilted": "Robot tilted", @@ -527,10 +533,12 @@ "sink_strainer_hoare": "Reinstall the water filter", "strainer_error": "Filter is wet or blocked", "temperature_protection": "Unit temperature protection", + "up_water_exception": "Water supply exception", "vertical_bumper_pressed": "Vertical bumper pressed", "vibrarise_jammed": "VibraRise jammed", "visual_sensor": "Camera error", "wall_sensor_dirty": "Wall sensor dirty", + "water_carriage_drop": "Water carriage dropped", "wheels_jammed": "Wheels jammed", "wheels_suspended": "Wheels suspended" } diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 32d7f21f2588c5..31f4e9ed36d3ad 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -1091,7 +1091,63 @@ None, ]), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'lidar_blocked', + 'bumper_stuck', + 'wheels_suspended', + 'cliff_sensor_error', + 'main_brush_jammed', + 'side_brush_jammed', + 'wheels_jammed', + 'robot_trapped', + 'no_dustbin', + 'strainer_error', + 'compass_error', + 'low_battery', + 'charging_error', + 'battery_error', + 'wall_sensor_dirty', + 'robot_tilted', + 'side_brush_error', + 'fan_error', + 'dock', + 'optical_flow_sensor_dirt', + 'vertical_bumper_pressed', + 'dock_locator_error', + 'return_to_dock_fail', + 'nogo_zone_detected', + 'visual_sensor', + 'light_touch', + 'vibrarise_jammed', + 'robot_on_carpet', + 'filter_blocked', + 'invisible_wall_detected', + 'cannot_cross_carpet', + 'internal_error', + 'collect_dust_error_3', + 'collect_dust_error_4', + 'mopping_roller_1', + 'mopping_roller_error_2', + 'clear_water_box_hoare', + 'dirty_water_box_hoare', + 'sink_strainer_hoare', + 'clear_water_box_exception', + 'clear_brush_exception', + 'clear_brush_exception_2', + 'filter_screen_exception', + 'mopping_roller_2', + 'up_water_exception', + 'drain_water_exception', + 'temperature_protection', + 'clean_carousel_exception', + 'clean_carousel_water_full', + 'water_carriage_drop', + 'check_clean_carouse', + 'audio_error', + ]), + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -1110,7 +1166,7 @@ 'object_id_base': 'Vacuum error', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Vacuum error', 'platform': 'roborock', @@ -1125,7 +1181,63 @@ # name: test_sensors[sensor.roborock_q10_s5_vacuum_error-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Roborock Q10 S5+ Vacuum error', + 'options': list([ + 'none', + 'lidar_blocked', + 'bumper_stuck', + 'wheels_suspended', + 'cliff_sensor_error', + 'main_brush_jammed', + 'side_brush_jammed', + 'wheels_jammed', + 'robot_trapped', + 'no_dustbin', + 'strainer_error', + 'compass_error', + 'low_battery', + 'charging_error', + 'battery_error', + 'wall_sensor_dirty', + 'robot_tilted', + 'side_brush_error', + 'fan_error', + 'dock', + 'optical_flow_sensor_dirt', + 'vertical_bumper_pressed', + 'dock_locator_error', + 'return_to_dock_fail', + 'nogo_zone_detected', + 'visual_sensor', + 'light_touch', + 'vibrarise_jammed', + 'robot_on_carpet', + 'filter_blocked', + 'invisible_wall_detected', + 'cannot_cross_carpet', + 'internal_error', + 'collect_dust_error_3', + 'collect_dust_error_4', + 'mopping_roller_1', + 'mopping_roller_error_2', + 'clear_water_box_hoare', + 'dirty_water_box_hoare', + 'sink_strainer_hoare', + 'clear_water_box_exception', + 'clear_brush_exception', + 'clear_brush_exception_2', + 'filter_screen_exception', + 'mopping_roller_2', + 'up_water_exception', + 'drain_water_exception', + 'temperature_protection', + 'clean_carousel_exception', + 'clean_carousel_water_full', + 'water_carriage_drop', + 'check_clean_carouse', + 'audio_error', + ]), }), 'context': , 'entity_id': 'sensor.roborock_q10_s5_vacuum_error', diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index f48d394c39d52d..9fed0df29cbfd5 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -40,7 +40,7 @@ async def test_q10_vacuum_error_updates_from_push( assert state.state == STATE_UNKNOWN assert fake_q10_vacuum.b01_q10_properties is not None - fake_q10_vacuum.b01_q10_properties.status.fault = "main_brush_jammed" + fake_q10_vacuum.b01_q10_properties.status.fault = 5 await fake_q10_vacuum.b01_q10_properties.refresh() await hass.async_block_till_done() From 749665597bbdc686c8c0dee0aaf93ab00b237db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 00:33:25 +0100 Subject: [PATCH 18/54] Remove battery sensor icon from Q10 vacuum icons configuration --- homeassistant/components/roborock/icons.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index e826f7d21d3730..fc3475056fbc61 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -40,9 +40,6 @@ } }, "sensor": { - "battery": { - "default": "mdi:battery" - }, "brush_remaining": { "default": "mdi:brush" }, From 531d97d4e139993dc9f67695d9ca9029c166920a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:08:05 +0100 Subject: [PATCH 19/54] Refactor Roborock state strings by removing unused keys for cleaner code --- homeassistant/components/roborock/strings.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 1f0f4b5f464d9c..4d198fdc570516 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -429,10 +429,7 @@ "charging": "[%key:common::state::charging%]", "charging_complete": "Charging complete", "charging_problem": "Charging problem", - "chargingstate": "[%key:common::state::charging%]", "cleaning": "Cleaning", - "cleaningstate": "Cleaning", - "creatingmapstate": "Creating map", "detaching_the_mop": "Detaching the mop", "device_offline": "Device offline", "docking": "Docking", @@ -440,35 +437,21 @@ "egg_attack": "Cupid mode", "emptying_the_bin": "Emptying the bin", "error": "[%key:common::state::error%]", - "faultstate": "Fault", "going_to_target": "Going to target", "going_to_wash_the_mop": "Going to wash the mop", "idle": "[%key:common::state::idle%]", "locked": "Locked", "manual_mode": "Manual mode", "mapping": "Mapping", - "mapsavestate": "Saving map", "paused": "[%key:common::state::paused%]", - "pausestate": "[%key:common::state::paused%]", - "relocationstate": "Relocating", "remote_control_active": "Remote control active", - "remoteingstate": "Remote control", "returning_home": "Returning home", - "robotmoping": "Mopping", - "robotsweepandmoping": "Sweep and mop", - "robotsweeping": "Sweeping", - "robottransitioning": "Transitioning", - "robotwaitcharge": "Waiting to charge", "segment_cleaning": "Segment cleaning", "shutting_down": "Shutting down", - "sleepstate": "Sleeping", "spot_cleaning": "Spot cleaning", - "standbystate": "Standby", "starting": "Starting", - "tochargestate": "Going to charge", "unknown": "Unknown", "updating": "Updating", - "upgradestate": "Upgrading", "washing_the_mop": "Washing the mop", "zoned_cleaning": "Zoned cleaning" } From 291b27ce782900e7fe1efa5595530d2dc12ee19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:14:15 +0100 Subject: [PATCH 20/54] Refactor Q10 vacuum error test to use update_from_dps for status updates --- tests/components/roborock/test_sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 9fed0df29cbfd5..1cf790f01ee91d 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -1,6 +1,7 @@ """Test Roborock Sensors.""" import pytest +from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNKNOWN, Platform @@ -40,9 +41,7 @@ async def test_q10_vacuum_error_updates_from_push( assert state.state == STATE_UNKNOWN assert fake_q10_vacuum.b01_q10_properties is not None - fake_q10_vacuum.b01_q10_properties.status.fault = 5 - - await fake_q10_vacuum.b01_q10_properties.refresh() + fake_q10_vacuum.b01_q10_properties.status.update_from_dps({B01_Q10_DP.FAULT: 5}) await hass.async_block_till_done() updated_state = hass.states.get(entity_id) From 43a69e8278f3d1b9fec6ad68c21b55e278f69aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:15:28 +0100 Subject: [PATCH 21/54] Remove unused sensor_remaining_life icon from Roborock icons.json --- homeassistant/components/roborock/icons.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index fc3475056fbc61..6a3707428db219 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -91,9 +91,6 @@ "sensor_life": { "default": "mdi:eye-outline" }, - "sensor_remaining_life": { - "default": "mdi:eye-outline" - }, "sensor_time_left": { "default": "mdi:eye-outline" }, From 3689ccf6bf095a9d241028559199208a14fb3c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:19:09 +0100 Subject: [PATCH 22/54] Add new vacuum state strings for improved user feedback --- homeassistant/components/roborock/strings.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 4d198fdc570516..72d990e4d63675 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -430,6 +430,7 @@ "charging_complete": "Charging complete", "charging_problem": "Charging problem", "cleaning": "Cleaning", + "creating_map": "Creating map", "detaching_the_mop": "Detaching the mop", "device_offline": "Device offline", "docking": "Docking", @@ -437,21 +438,33 @@ "egg_attack": "Cupid mode", "emptying_the_bin": "Emptying the bin", "error": "[%key:common::state::error%]", + "fault": "Fault", + "going_to_charge": "Going to charge", "going_to_target": "Going to target", "going_to_wash_the_mop": "Going to wash the mop", "idle": "[%key:common::state::idle%]", "locked": "Locked", "manual_mode": "Manual mode", "mapping": "Mapping", + "mopping": "Mopping", "paused": "[%key:common::state::paused%]", + "relocating": "Relocating", + "remote_control": "Remote control", "remote_control_active": "Remote control active", "returning_home": "Returning home", + "saving_map": "Saving map", "segment_cleaning": "Segment cleaning", "shutting_down": "Shutting down", + "sleeping": "Sleeping", "spot_cleaning": "Spot cleaning", + "standby": "Standby", "starting": "Starting", + "sweep_and_mop": "Sweep and mop", + "sweeping": "Sweeping", + "transitioning": "Transitioning", "unknown": "Unknown", "updating": "Updating", + "waiting_to_charge": "Waiting to charge", "washing_the_mop": "Washing the mop", "zoned_cleaning": "Zoned cleaning" } From 8a81bf0a973b57aad386c7b81addb1d1ba469dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:29:45 +0100 Subject: [PATCH 23/54] tests: update roborock sensor snapshots with normalized YXDeviceState enum values --- .../roborock/snapshots/test_sensor.ambr | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 31f4e9ed36d3ad..90149ed3892c12 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -832,24 +832,24 @@ 'capabilities': dict({ 'options': list([ 'unknown', - 'sleepstate', - 'standbystate', - 'cleaningstate', - 'tochargestate', - 'remoteingstate', - 'chargingstate', - 'pausestate', - 'faultstate', - 'upgradestate', + 'sleeping', + 'standby', + 'cleaning', + 'going_to_charge', + 'remote_control', + 'charging', + 'paused', + 'fault', + 'updating', 'dusting', - 'creatingmapstate', - 'mapsavestate', - 'relocationstate', - 'robotsweeping', - 'robotmoping', - 'robotsweepandmoping', - 'robottransitioning', - 'robotwaitcharge', + 'creating_map', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', ]), }), 'config_entry_id': , @@ -889,24 +889,24 @@ 'friendly_name': 'Roborock Q10 S5+ Status', 'options': list([ 'unknown', - 'sleepstate', - 'standbystate', - 'cleaningstate', - 'tochargestate', - 'remoteingstate', - 'chargingstate', - 'pausestate', - 'faultstate', - 'upgradestate', + 'sleeping', + 'standby', + 'cleaning', + 'going_to_charge', + 'remote_control', + 'charging', + 'paused', + 'fault', + 'updating', 'dusting', - 'creatingmapstate', - 'mapsavestate', - 'relocationstate', - 'robotsweeping', - 'robotmoping', - 'robotsweepandmoping', - 'robottransitioning', - 'robotwaitcharge', + 'creating_map', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', ]), }), 'context': , @@ -914,7 +914,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'chargingstate', + 'state': 'charging', }) # --- # name: test_sensors[sensor.roborock_q10_s5_total_cleaning_area-entry] From 2ea8495ba60e12d7d803f11295ab470d3bc4444e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:39:50 +0100 Subject: [PATCH 24/54] roborock: add dedicated q10_status translation key for YXDeviceState enum sensor --- homeassistant/components/roborock/sensor.py | 2 +- .../components/roborock/strings.json | 24 +++ .../roborock/snapshots/test_sensor.ambr | 188 +++++++++--------- 3 files changed, 119 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index d14ae8a335beb6..8972e33d452825 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -428,7 +428,7 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: Q10_B01_SENSOR_DESCRIPTIONS = [ RoborockSensorDescriptionQ10( key="status", - translation_key="status", + translation_key="q10_status", device_class=SensorDeviceClass.ENUM, value_fn=lambda data: data.status.value if data.status is not None else None, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 72d990e4d63675..f2fb2717030b23 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -392,6 +392,30 @@ "mop_life_time_left": { "name": "Mop life time left" }, + "q10_status": { + "name": "Status", + "state": { + "charging": "[%key:common::state::charging%]", + "cleaning": "[%key:component::roborock::entity::sensor::status::state::cleaning%]", + "creating_map": "[%key:component::roborock::entity::sensor::status::state::creating_map%]", + "dusting": "[%key:component::roborock::entity::sensor::status::state::dusting%]", + "fault": "[%key:component::roborock::entity::sensor::status::state::fault%]", + "going_to_charge": "[%key:component::roborock::entity::sensor::status::state::going_to_charge%]", + "mopping": "[%key:component::roborock::entity::sensor::status::state::mopping%]", + "paused": "[%key:common::state::paused%]", + "relocating": "[%key:component::roborock::entity::sensor::status::state::relocating%]", + "remote_control": "[%key:component::roborock::entity::sensor::status::state::remote_control%]", + "saving_map": "[%key:component::roborock::entity::sensor::status::state::saving_map%]", + "sleeping": "[%key:component::roborock::entity::sensor::status::state::sleeping%]", + "standby": "[%key:component::roborock::entity::sensor::status::state::standby%]", + "sweep_and_mop": "[%key:component::roborock::entity::sensor::status::state::sweep_and_mop%]", + "sweeping": "[%key:component::roborock::entity::sensor::status::state::sweeping%]", + "transitioning": "[%key:component::roborock::entity::sensor::status::state::transitioning%]", + "unknown": "[%key:component::roborock::entity::sensor::status::state::unknown%]", + "updating": "[%key:component::roborock::entity::sensor::status::state::updating%]", + "waiting_to_charge": "[%key:component::roborock::entity::sensor::status::state::waiting_to_charge%]" + } + }, "q7_status": { "name": "Status", "state": { diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 90149ed3892c12..853193e9c4d185 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -407,6 +407,100 @@ 'state': '3.55', }) # --- +# name: test_sensors[sensor.roborock_q10_s5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'unknown', + 'sleeping', + 'standby', + 'cleaning', + 'going_to_charge', + 'remote_control', + 'charging', + 'paused', + 'fault', + 'updating', + 'dusting', + 'creating_map', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'q10_status', + 'unique_id': 'status_q10_duid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock Q10 S5+', + 'options': list([ + 'unknown', + 'sleeping', + 'standby', + 'cleaning', + 'going_to_charge', + 'remote_control', + 'charging', + 'paused', + 'fault', + 'updating', + 'dusting', + 'creating_map', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- # name: test_sensors[sensor.roborock_q10_s5_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -823,100 +917,6 @@ 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': list([ - None, - ]), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'unknown', - 'sleeping', - 'standby', - 'cleaning', - 'going_to_charge', - 'remote_control', - 'charging', - 'paused', - 'fault', - 'updating', - 'dusting', - 'creating_map', - 'saving_map', - 'relocating', - 'sweeping', - 'mopping', - 'sweep_and_mop', - 'transitioning', - 'waiting_to_charge', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'object_id_base': 'Status', - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Status', - 'platform': 'roborock', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'status', - 'unique_id': 'status_q10_duid', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.roborock_q10_s5_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Roborock Q10 S5+ Status', - 'options': list([ - 'unknown', - 'sleeping', - 'standby', - 'cleaning', - 'going_to_charge', - 'remote_control', - 'charging', - 'paused', - 'fault', - 'updating', - 'dusting', - 'creating_map', - 'saving_map', - 'relocating', - 'sweeping', - 'mopping', - 'sweep_and_mop', - 'transitioning', - 'waiting_to_charge', - ]), - }), - 'context': , - 'entity_id': 'sensor.roborock_q10_s5_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'charging', - }) -# --- # name: test_sensors[sensor.roborock_q10_s5_total_cleaning_area-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ From 8bcd3e266db02b6d47b13489b27cb78a1125ddff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:41:22 +0100 Subject: [PATCH 25/54] roborock: handle unknown Q10 error codes gracefully in _q10_error_value_fn --- homeassistant/components/roborock/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 8972e33d452825..480b17b6327b83 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -94,7 +94,11 @@ def _q10_error_value_fn(status: Q10StatusTrait) -> str | None: return None if isinstance(fault, str): return fault - return RoborockErrorCode(fault).name + try: + return RoborockErrorCode(fault).name + except ValueError: + _LOGGER.debug("Unknown Roborock error code reported for Q10: %s", fault) + return None def _dock_error_value_fn(state: DeviceState) -> str | None: From 41b852a4d8bd2a95eb57bc179d626dd51ffc0cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 15:45:47 +0100 Subject: [PATCH 26/54] roborock: also catch TypeError in _q10_error_value_fn for unknown Q10 fault codes --- homeassistant/components/roborock/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 480b17b6327b83..cc8e3a11931c22 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -96,7 +96,7 @@ def _q10_error_value_fn(status: Q10StatusTrait) -> str | None: return fault try: return RoborockErrorCode(fault).name - except ValueError: + except ValueError, TypeError: _LOGGER.debug("Unknown Roborock error code reported for Q10: %s", fault) return None From c117395ccd7ece67d58583d2d01d8ebe83c5391d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 19:03:47 +0100 Subject: [PATCH 27/54] Add status sensor for Roborock Q10 S5+ with state options --- .../roborock/snapshots/test_sensor.ambr | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 853193e9c4d185..17ce6330617d71 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -917,6 +917,100 @@ 'state': 'unknown', }) # --- +# name: test_sensors[sensor.roborock_q10_s5_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'unknown', + 'sleeping', + 'standby', + 'cleaning', + 'going_to_charge', + 'remote_control', + 'charging', + 'paused', + 'fault', + 'updating', + 'dusting', + 'creating_map', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.roborock_q10_s5_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Status', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'q10_status', + 'unique_id': 'status_q10_duid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock Q10 S5+ Status', + 'options': list([ + 'unknown', + 'sleeping', + 'standby', + 'cleaning', + 'going_to_charge', + 'remote_control', + 'charging', + 'paused', + 'fault', + 'updating', + 'dusting', + 'creating_map', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- # name: test_sensors[sensor.roborock_q10_s5_total_cleaning_area-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ From 6b07457e98726506459220c59a086f89ce428f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 21:02:30 +0100 Subject: [PATCH 28/54] bump python-roborock requirement to version 5.0.0 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index e1c39ba40b13c4..04f4fbfa29a120 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -20,7 +20,7 @@ "loggers": ["roborock"], "quality_scale": "silver", "requirements": [ - "python-roborock==4.26.3", + "python-roborock==5.0.0", "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 04e53659285d1b..2a4f63485a89e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2660,7 +2660,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==4.26.3 +python-roborock==5.0.0 # homeassistant.components.smarttub python-smarttub==0.0.47 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edfe1334b05edf..463e39ba8ad635 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2256,7 +2256,7 @@ python-pooldose==0.8.6 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==4.26.3 +python-roborock==5.0.0 # homeassistant.components.smarttub python-smarttub==0.0.47 From 9a49cc9c57655a5686df15fa13c0cd414f75401e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 21:05:07 +0100 Subject: [PATCH 29/54] Update Q10 state mappings and mock data for Roborock vacuum integration --- homeassistant/components/roborock/vacuum.py | 34 ++++++++++----------- tests/components/roborock/conftest.py | 8 ++++- tests/components/roborock/mock_data.py | 4 +-- tests/components/roborock/test_vacuum.py | 6 ++-- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index e0ed13b631ab1e..fb55d81d589280 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -80,23 +80,23 @@ } Q10_STATE_CODE_TO_STATE = { - YXDeviceState.SLEEP_STATE: VacuumActivity.IDLE, - YXDeviceState.STANDBY_STATE: VacuumActivity.IDLE, - YXDeviceState.CLEANING_STATE: VacuumActivity.CLEANING, - YXDeviceState.TO_CHARGE_STATE: VacuumActivity.RETURNING, - YXDeviceState.REMOTEING_STATE: VacuumActivity.CLEANING, - YXDeviceState.CHARGING_STATE: VacuumActivity.DOCKED, - YXDeviceState.PAUSE_STATE: VacuumActivity.PAUSED, - YXDeviceState.FAULT_STATE: VacuumActivity.ERROR, - YXDeviceState.UPGRADE_STATE: VacuumActivity.DOCKED, - YXDeviceState.DUSTING: VacuumActivity.DOCKED, - YXDeviceState.CREATING_MAP_STATE: VacuumActivity.CLEANING, - YXDeviceState.RE_LOCATION_STATE: VacuumActivity.CLEANING, - YXDeviceState.ROBOT_SWEEPING: VacuumActivity.CLEANING, - YXDeviceState.ROBOT_MOPING: VacuumActivity.CLEANING, - YXDeviceState.ROBOT_SWEEP_AND_MOPING: VacuumActivity.CLEANING, - YXDeviceState.ROBOT_TRANSITIONING: VacuumActivity.CLEANING, - YXDeviceState.ROBOT_WAIT_CHARGE: VacuumActivity.DOCKED, + YXDeviceState.SLEEPING: VacuumActivity.IDLE, + YXDeviceState.IDLE: VacuumActivity.IDLE, + YXDeviceState.CLEANING: VacuumActivity.CLEANING, + YXDeviceState.RETURNING_HOME: VacuumActivity.RETURNING, + YXDeviceState.REMOTE_CONTROL_ACTIVE: VacuumActivity.CLEANING, + YXDeviceState.CHARGING: VacuumActivity.DOCKED, + YXDeviceState.PAUSED: VacuumActivity.PAUSED, + YXDeviceState.ERROR: VacuumActivity.ERROR, + YXDeviceState.UPDATING: VacuumActivity.DOCKED, + YXDeviceState.EMPTYING_THE_BIN: VacuumActivity.DOCKED, + YXDeviceState.MAPPING: VacuumActivity.CLEANING, + YXDeviceState.RELOCATING: VacuumActivity.CLEANING, + YXDeviceState.SWEEPING: VacuumActivity.CLEANING, + YXDeviceState.MOPPING: VacuumActivity.CLEANING, + YXDeviceState.SWEEP_AND_MOP: VacuumActivity.CLEANING, + YXDeviceState.TRANSITIONING: VacuumActivity.CLEANING, + YXDeviceState.WAITING_TO_CHARGE: VacuumActivity.DOCKED, } PARALLEL_UPDATES = 0 diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index ead00658378ca0..eb8604867762da 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -185,7 +185,13 @@ def create_b01_q10_trait() -> Mock: q10_trait.command = AsyncMock() def _raw_value(value: Any) -> Any: - return value.value if hasattr(value, "value") else value + return ( + value.code + if hasattr(value, "code") + else value.value + if hasattr(value, "value") + else value + ) async def refresh_side_effect() -> None: """Simulate a device push via the public Q10 DPS update API.""" diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 30c2c80d24d3c8..63258a7c0f155c 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1570,8 +1570,8 @@ clean_time=120, clean_area=15, battery=100, - status=YXDeviceState.CHARGING_STATE, + status=YXDeviceState.CHARGING, fan_level=YXFanLevel.BALANCED, - water_level=YXWaterLevel.MIDDLE, + water_level=YXWaterLevel.MEDIUM, clean_count=1, ) diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 8cc32b0eb60eda..9e8dc2e90de2a4 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -932,14 +932,14 @@ async def test_q10_push_status_update( assert fake_q10_vacuum.b01_q10_properties is not None api = fake_q10_vacuum.b01_q10_properties - # Verify initial state is "docked" (from Q10_STATUS fixture: CHARGING_STATE) + # Verify initial state is "docked" (from Q10_STATUS fixture: CHARGING) vacuum = hass.states.get(Q10_ENTITY_ID) assert vacuum assert vacuum.state == "docked" # Simulate the device pushing a status change via DPS data # (e.g. user started cleaning from the Roborock app) - api.status.update_from_dps({B01_Q10_DP.STATUS: 5}) # CLEANING_STATE + api.status.update_from_dps({B01_Q10_DP.STATUS: 5}) # CLEANING await hass.async_block_till_done() # Verify the entity state updated to "cleaning" @@ -948,7 +948,7 @@ async def test_q10_push_status_update( assert vacuum.state == "cleaning" # Simulate returning to dock - api.status.update_from_dps({B01_Q10_DP.STATUS: 6}) # TO_CHARGE_STATE + api.status.update_from_dps({B01_Q10_DP.STATUS: 6}) # RETURNING_HOME await hass.async_block_till_done() vacuum = hass.states.get(Q10_ENTITY_ID) From 161e5205e38ab695a6518e291bac0c3d01a9afd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 21:05:46 +0100 Subject: [PATCH 30/54] Remove deprecated sensor entries and update state options for Roborock Q10 S5 --- .../roborock/snapshots/test_sensor.ambr | 118 ++---------------- 1 file changed, 12 insertions(+), 106 deletions(-) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 17ce6330617d71..b52d01bfd5d62d 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -407,100 +407,6 @@ 'state': '3.55', }) # --- -# name: test_sensors[sensor.roborock_q10_s5-entry] - EntityRegistryEntrySnapshot({ - 'aliases': list([ - None, - ]), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'unknown', - 'sleeping', - 'standby', - 'cleaning', - 'going_to_charge', - 'remote_control', - 'charging', - 'paused', - 'fault', - 'updating', - 'dusting', - 'creating_map', - 'saving_map', - 'relocating', - 'sweeping', - 'mopping', - 'sweep_and_mop', - 'transitioning', - 'waiting_to_charge', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'object_id_base': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'roborock', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'q10_status', - 'unique_id': 'status_q10_duid', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.roborock_q10_s5-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Roborock Q10 S5+', - 'options': list([ - 'unknown', - 'sleeping', - 'standby', - 'cleaning', - 'going_to_charge', - 'remote_control', - 'charging', - 'paused', - 'fault', - 'updating', - 'dusting', - 'creating_map', - 'saving_map', - 'relocating', - 'sweeping', - 'mopping', - 'sweep_and_mop', - 'transitioning', - 'waiting_to_charge', - ]), - }), - 'context': , - 'entity_id': 'sensor.roborock_q10_s5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'charging', - }) -# --- # name: test_sensors[sensor.roborock_q10_s5_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -927,16 +833,16 @@ 'options': list([ 'unknown', 'sleeping', - 'standby', + 'idle', 'cleaning', - 'going_to_charge', - 'remote_control', + 'returning_home', + 'remote_control_active', 'charging', 'paused', - 'fault', + 'error', 'updating', - 'dusting', - 'creating_map', + 'emptying_the_bin', + 'mapping', 'saving_map', 'relocating', 'sweeping', @@ -984,16 +890,16 @@ 'options': list([ 'unknown', 'sleeping', - 'standby', + 'idle', 'cleaning', - 'going_to_charge', - 'remote_control', + 'returning_home', + 'remote_control_active', 'charging', 'paused', - 'fault', + 'error', 'updating', - 'dusting', - 'creating_map', + 'emptying_the_bin', + 'mapping', 'saving_map', 'relocating', 'sweeping', From 1e10fc035d17c2ac2865c5fc8e5a01a7cb0d74d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 23:52:26 +0100 Subject: [PATCH 31/54] Update Roborock vacuum status messages for improved clarity and consistency --- homeassistant/components/roborock/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index f2fb2717030b23..acf479e1a3483a 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -397,17 +397,17 @@ "state": { "charging": "[%key:common::state::charging%]", "cleaning": "[%key:component::roborock::entity::sensor::status::state::cleaning%]", - "creating_map": "[%key:component::roborock::entity::sensor::status::state::creating_map%]", - "dusting": "[%key:component::roborock::entity::sensor::status::state::dusting%]", - "fault": "[%key:component::roborock::entity::sensor::status::state::fault%]", - "going_to_charge": "[%key:component::roborock::entity::sensor::status::state::going_to_charge%]", + "emptying_the_bin": "[%key:component::roborock::entity::sensor::status::state::emptying_the_bin%]", + "error": "[%key:component::roborock::entity::sensor::status::state::error%]", + "idle": "[%key:component::roborock::entity::sensor::status::state::idle%]", + "mapping": "[%key:component::roborock::entity::sensor::status::state::mapping%]", "mopping": "[%key:component::roborock::entity::sensor::status::state::mopping%]", "paused": "[%key:common::state::paused%]", "relocating": "[%key:component::roborock::entity::sensor::status::state::relocating%]", - "remote_control": "[%key:component::roborock::entity::sensor::status::state::remote_control%]", + "remote_control_active": "[%key:component::roborock::entity::sensor::status::state::remote_control_active%]", + "returning_home": "[%key:component::roborock::entity::sensor::status::state::returning_home%]", "saving_map": "[%key:component::roborock::entity::sensor::status::state::saving_map%]", "sleeping": "[%key:component::roborock::entity::sensor::status::state::sleeping%]", - "standby": "[%key:component::roborock::entity::sensor::status::state::standby%]", "sweep_and_mop": "[%key:component::roborock::entity::sensor::status::state::sweep_and_mop%]", "sweeping": "[%key:component::roborock::entity::sensor::status::state::sweeping%]", "transitioning": "[%key:component::roborock::entity::sensor::status::state::transitioning%]", From 71d97041d4b25b342936359b8e0f99648c928f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 22 Mar 2026 23:57:33 +0100 Subject: [PATCH 32/54] Refactor Roborock vacuum status strings for consistency and clarity --- homeassistant/components/roborock/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index acf479e1a3483a..1a0e3383073ee1 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -398,8 +398,8 @@ "charging": "[%key:common::state::charging%]", "cleaning": "[%key:component::roborock::entity::sensor::status::state::cleaning%]", "emptying_the_bin": "[%key:component::roborock::entity::sensor::status::state::emptying_the_bin%]", - "error": "[%key:component::roborock::entity::sensor::status::state::error%]", - "idle": "[%key:component::roborock::entity::sensor::status::state::idle%]", + "error": "[%key:common::state::error%]", + "idle": "[%key:common::state::idle%]", "mapping": "[%key:component::roborock::entity::sensor::status::state::mapping%]", "mopping": "[%key:component::roborock::entity::sensor::status::state::mopping%]", "paused": "[%key:common::state::paused%]", From 752c10e2767b23eb7849f39d02be77fdaa02301c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:01:13 +0100 Subject: [PATCH 33/54] Refactor Roborock vacuum status strings for consistency and clarity --- homeassistant/components/roborock/strings.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 1a0e3383073ee1..c2b7eaec9a86bf 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -454,7 +454,7 @@ "charging_complete": "Charging complete", "charging_problem": "Charging problem", "cleaning": "Cleaning", - "creating_map": "Creating map", + "creating_map": "[%key:component::roborock::entity::sensor::status::state::creating_map%]", "detaching_the_mop": "Detaching the mop", "device_offline": "Device offline", "docking": "Docking", @@ -462,8 +462,6 @@ "egg_attack": "Cupid mode", "emptying_the_bin": "Emptying the bin", "error": "[%key:common::state::error%]", - "fault": "Fault", - "going_to_charge": "Going to charge", "going_to_target": "Going to target", "going_to_wash_the_mop": "Going to wash the mop", "idle": "[%key:common::state::idle%]", From 8540e982ec3afd6aafaeb9f3984cee00899b2afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:16:24 +0100 Subject: [PATCH 34/54] Refactor Roborock vacuum status strings for consistency and clarity --- homeassistant/components/roborock/strings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index c2b7eaec9a86bf..2e50e032fefd3a 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -454,7 +454,6 @@ "charging_complete": "Charging complete", "charging_problem": "Charging problem", "cleaning": "Cleaning", - "creating_map": "[%key:component::roborock::entity::sensor::status::state::creating_map%]", "detaching_the_mop": "Detaching the mop", "device_offline": "Device offline", "docking": "Docking", From d48a82b834daa652ecd927fd309e6dfcf6d9b07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:23:13 +0100 Subject: [PATCH 35/54] Refactor Roborock vacuum status strings for consistency and clarity --- homeassistant/components/roborock/strings.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 2e50e032fefd3a..cab649669c0bfb 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -457,7 +457,6 @@ "detaching_the_mop": "Detaching the mop", "device_offline": "Device offline", "docking": "Docking", - "dusting": "Dusting", "egg_attack": "Cupid mode", "emptying_the_bin": "Emptying the bin", "error": "[%key:common::state::error%]", @@ -470,7 +469,6 @@ "mopping": "Mopping", "paused": "[%key:common::state::paused%]", "relocating": "Relocating", - "remote_control": "Remote control", "remote_control_active": "Remote control active", "returning_home": "Returning home", "saving_map": "Saving map", @@ -478,7 +476,6 @@ "shutting_down": "Shutting down", "sleeping": "Sleeping", "spot_cleaning": "Spot cleaning", - "standby": "Standby", "starting": "Starting", "sweep_and_mop": "Sweep and mop", "sweeping": "Sweeping", @@ -599,7 +596,6 @@ "rinsing": "Rinsing", "soaking": "Soaking", "spinning": "Spinning", - "standby": "[%key:common::state::standby%]", "under_delay_start": "Delayed start", "waiting_for_aftercare": "Waiting for aftercare", "washing": "Washing", From c290c91cf3824d80330d78724574ed47eb78831b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:27:23 +0100 Subject: [PATCH 36/54] Enhance Q10 DPS update API simulation with new data handling --- tests/components/roborock/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index eb8604867762da..6edbcb6f639d22 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -194,7 +194,9 @@ def _raw_value(value: Any) -> Any: ) async def refresh_side_effect() -> None: - """Simulate a device push via the public Q10 DPS update API.""" + """Simulate a device push via the public Q10 DPS update API with new data.""" + # Mutate clean_count to simulate a new cleaning cycle + status.clean_count += 1 dps: dict[B01_Q10_DP, Any] = { B01_Q10_DP.STATUS: _raw_value(status.status), B01_Q10_DP.BATTERY: status.battery, From 95572be8f66bae0e3df1ca7c67243c636b39e231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:28:45 +0100 Subject: [PATCH 37/54] Update Q10 vacuum error test to simulate device push with fault mutation --- tests/components/roborock/test_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 1cf790f01ee91d..7b4d7431ac2c69 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -40,8 +40,11 @@ async def test_q10_vacuum_error_updates_from_push( assert state is not None assert state.state == STATE_UNKNOWN + assert fake_q10_vacuum.b01_q10_properties is not None - fake_q10_vacuum.b01_q10_properties.status.update_from_dps({B01_Q10_DP.FAULT: 5}) + # Mutate the fault value, then call refresh to simulate a device push + fake_q10_vacuum.b01_q10_properties.status.fault = 5 + await fake_q10_vacuum.b01_q10_properties.refresh() await hass.async_block_till_done() updated_state = hass.states.get(entity_id) From 15fbb5ec3fa81dc5bf88203255cc963a75050df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:31:42 +0100 Subject: [PATCH 38/54] Fix exception handling in Q10 error value function --- homeassistant/components/roborock/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index cc8e3a11931c22..d01bd4f79262de 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -96,7 +96,7 @@ def _q10_error_value_fn(status: Q10StatusTrait) -> str | None: return fault try: return RoborockErrorCode(fault).name - except ValueError, TypeError: + except (ValueError, TypeError): _LOGGER.debug("Unknown Roborock error code reported for Q10: %s", fault) return None From 240c3b71ae6039bd591bdb561b8dc03115600f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:33:46 +0100 Subject: [PATCH 39/54] Add standby state string for Roborock vacuum --- homeassistant/components/roborock/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index cab649669c0bfb..54c68ed2a20232 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -596,6 +596,7 @@ "rinsing": "Rinsing", "soaking": "Soaking", "spinning": "Spinning", + "standby": "[%key:common::state::standby%]", "under_delay_start": "Delayed start", "waiting_for_aftercare": "Waiting for aftercare", "washing": "Washing", From 5215f4963e1125cef6d9791aefa7dd09806b4a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:36:44 +0100 Subject: [PATCH 40/54] Remove unused Q10 error value function to clean up code --- homeassistant/components/roborock/sensor.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index d01bd4f79262de..d1b2d8379553cf 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -88,17 +88,6 @@ class RoborockSensorDescriptionQ10(SensorEntityDescription): value_fn: Callable[[Q10StatusTrait], StateType] -def _q10_error_value_fn(status: Q10StatusTrait) -> str | None: - fault = getattr(status, "fault", None) - if fault is None: - return None - if isinstance(fault, str): - return fault - try: - return RoborockErrorCode(fault).name - except (ValueError, TypeError): - _LOGGER.debug("Unknown Roborock error code reported for Q10: %s", fault) - return None def _dock_error_value_fn(state: DeviceState) -> str | None: From 05d05f68f087f8ddc3487163421cba07e7e658a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 08:40:07 +0100 Subject: [PATCH 41/54] Update Q10 vacuum error value function to return fault status and adjust test assertion for unknown state --- homeassistant/components/roborock/sensor.py | 2 +- tests/components/roborock/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index d1b2d8379553cf..f1c2fc540bfb2b 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -513,7 +513,7 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: RoborockSensorDescriptionQ10( key="vacuum_error", translation_key="vacuum_error", - value_fn=_q10_error_value_fn, + value_fn=lambda status: status.fault, device_class=SensorDeviceClass.ENUM, options=RoborockErrorCode.keys(), entity_category=EntityCategory.DIAGNOSTIC, diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 7b4d7431ac2c69..64cf14795b7dd4 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -49,4 +49,4 @@ async def test_q10_vacuum_error_updates_from_push( updated_state = hass.states.get(entity_id) assert updated_state is not None - assert updated_state.state == "main_brush_jammed" + assert updated_state.state == "unknown" From 2f05f5e1688dce11243ea999eb8804b38e7dc404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 12:50:08 +0100 Subject: [PATCH 42/54] Remove unused import from Q10 vacuum sensor test --- tests/components/roborock/test_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 64cf14795b7dd4..15ad1c69de537b 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -1,7 +1,6 @@ """Test Roborock Sensors.""" import pytest -from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNKNOWN, Platform @@ -40,7 +39,6 @@ async def test_q10_vacuum_error_updates_from_push( assert state is not None assert state.state == STATE_UNKNOWN - assert fake_q10_vacuum.b01_q10_properties is not None # Mutate the fault value, then call refresh to simulate a device push fake_q10_vacuum.b01_q10_properties.status.fault = 5 From 25606aaef0ffb93a5441a77c2211cdc88764d5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 23 Mar 2026 13:10:37 +0100 Subject: [PATCH 43/54] Remove unnecessary blank lines in Roborock sensor description --- homeassistant/components/roborock/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index f1c2fc540bfb2b..8ee89ed04a94c6 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -88,8 +88,6 @@ class RoborockSensorDescriptionQ10(SensorEntityDescription): value_fn: Callable[[Q10StatusTrait], StateType] - - def _dock_error_value_fn(state: DeviceState) -> str | None: if ( status := state.status.dock_error_status From febc0f7eb8448a41fd9768d3bf7a8881f361a2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:05:15 +0200 Subject: [PATCH 44/54] Include FAULT DP in roborock Q10 refresh push simulation Add B01_Q10_DP.FAULT to the DPS dict in refresh_side_effect so that tests simulating a device push can validate vacuum error state updates. --- tests/components/roborock/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 6edbcb6f639d22..e5a3668d0693ea 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -212,6 +212,8 @@ async def refresh_side_effect() -> None: dps[B01_Q10_DP.NOT_DISTURB] = _raw_value(status.not_disturb) if hasattr(status, "dust_switch"): dps[B01_Q10_DP.DUST_SWITCH] = _raw_value(status.dust_switch) + if hasattr(status, "fault"): + dps[B01_Q10_DP.FAULT] = _raw_value(status.fault) status.update_from_dps(dps) q10_trait.refresh = AsyncMock(side_effect=refresh_side_effect) From 6ecdd9b9219a42f1930f99e2756f22cc90c87c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:09:11 +0200 Subject: [PATCH 45/54] Update roborock sensor snapshots for Q10 S5+ entities Regenerate snapshots after entity ID cleanup: removed 10 stale numbered entries (roborock_q10_s5, _2..._5) and generated their proper named replacements. --- .../roborock/snapshots/test_sensor.ambr | 308 +++++++++--------- 1 file changed, 154 insertions(+), 154 deletions(-) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index b200c1d52deaef..c02245d7aab223 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -407,35 +407,13 @@ 'state': '3.55', }) # --- -# name: test_sensors[sensor.roborock_q10_s5-entry] +# name: test_sensors[sensor.roborock_q10_s5_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'unknown', - 'sleeping', - 'idle', - 'cleaning', - 'returning_home', - 'remote_control_active', - 'charging', - 'paused', - 'error', - 'updating', - 'emptying_the_bin', - 'mapping', - 'saving_map', - 'relocating', - 'sweeping', - 'mopping', - 'sweep_and_mop', - 'transitioning', - 'waiting_to_charge', - ]), - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -443,7 +421,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5', + 'entity_id': 'sensor.roborock_q10_s5_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -451,57 +429,37 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Battery', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': None, + 'original_name': 'Battery', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'q10_status', - 'unique_id': 'status_q10_duid', - 'unit_of_measurement': None, + 'translation_key': None, + 'unique_id': 'battery_q10_duid', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.roborock_q10_s5-state] +# name: test_sensors[sensor.roborock_q10_s5_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Roborock Q10 S5+', - 'options': list([ - 'unknown', - 'sleeping', - 'idle', - 'cleaning', - 'returning_home', - 'remote_control_active', - 'charging', - 'paused', - 'error', - 'updating', - 'emptying_the_bin', - 'mapping', - 'saving_map', - 'relocating', - 'sweeping', - 'mopping', - 'sweep_and_mop', - 'transitioning', - 'waiting_to_charge', - ]), + 'device_class': 'battery', + 'friendly_name': 'Roborock Q10 S5+ Battery', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5', + 'entity_id': 'sensor.roborock_q10_s5_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'charging', + 'state': '100', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_2-entry] +# name: test_sensors[sensor.roborock_q10_s5_cleaning_area-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -515,7 +473,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_2', + 'entity_id': 'sensor.roborock_q10_s5_cleaning_area', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -523,36 +481,36 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Cleaning area', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Cleaning area', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'main_brush_life', - 'unique_id': 'main_brush_life_q10_duid', - 'unit_of_measurement': '%', + 'translation_key': 'cleaning_area', + 'unique_id': 'cleaning_area_q10_duid', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.roborock_q10_s5_2-state] +# name: test_sensors[sensor.roborock_q10_s5_cleaning_area-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+', - 'unit_of_measurement': '%', + 'friendly_name': 'Roborock Q10 S5+ Cleaning area', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_2', + 'entity_id': 'sensor.roborock_q10_s5_cleaning_area', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '15', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_3-entry] +# name: test_sensors[sensor.roborock_q10_s5_cleaning_progress-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -566,7 +524,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_3', + 'entity_id': 'sensor.roborock_q10_s5_cleaning_progress', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -574,36 +532,36 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Cleaning progress', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Cleaning progress', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'side_brush_life', - 'unique_id': 'side_brush_life_q10_duid', + 'translation_key': 'clean_percent', + 'unique_id': 'clean_percent_q10_duid', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_3-state] +# name: test_sensors[sensor.roborock_q10_s5_cleaning_progress-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+', + 'friendly_name': 'Roborock Q10 S5+ Cleaning progress', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_3', + 'entity_id': 'sensor.roborock_q10_s5_cleaning_progress', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_4-entry] +# name: test_sensors[sensor.roborock_q10_s5_cleaning_time-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -617,7 +575,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_4', + 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -625,36 +583,43 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Cleaning time', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': None, + 'original_name': 'Cleaning time', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'filter_life', - 'unique_id': 'filter_life_q10_duid', - 'unit_of_measurement': '%', + 'translation_key': 'cleaning_time', + 'unique_id': 'cleaning_time_q10_duid', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.roborock_q10_s5_4-state] +# name: test_sensors[sensor.roborock_q10_s5_cleaning_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Cleaning time', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_4', + 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.0', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_5-entry] +# name: test_sensors[sensor.roborock_q10_s5_filter_remaining_life-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -668,7 +633,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_5', + 'entity_id': 'sensor.roborock_q10_s5_filter_remaining_life', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -676,36 +641,36 @@ 'labels': set({ }), 'name': None, - 'object_id_base': None, + 'object_id_base': 'Filter remaining life', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Filter remaining life', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'sensor_life', - 'unique_id': 'sensor_life_q10_duid', + 'translation_key': 'filter_life', + 'unique_id': 'filter_life_q10_duid', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_5-state] +# name: test_sensors[sensor.roborock_q10_s5_filter_remaining_life-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+', + 'friendly_name': 'Roborock Q10 S5+ Filter remaining life', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_5', + 'entity_id': 'sensor.roborock_q10_s5_filter_remaining_life', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_battery-entry] +# name: test_sensors[sensor.roborock_q10_s5_main_brush_remaining_life-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -719,7 +684,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_battery', + 'entity_id': 'sensor.roborock_q10_s5_main_brush_remaining_life', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -727,37 +692,36 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Battery', + 'object_id_base': 'Main brush remaining life', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Battery', + 'original_name': 'Main brush remaining life', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'battery_q10_duid', + 'translation_key': 'main_brush_life', + 'unique_id': 'main_brush_life_q10_duid', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_battery-state] +# name: test_sensors[sensor.roborock_q10_s5_main_brush_remaining_life-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Roborock Q10 S5+ Battery', + 'friendly_name': 'Roborock Q10 S5+ Main brush remaining life', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_battery', + 'entity_id': 'sensor.roborock_q10_s5_main_brush_remaining_life', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_cleaning_area-entry] +# name: test_sensors[sensor.roborock_q10_s5_sensor_remaining_life-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -771,7 +735,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_cleaning_area', + 'entity_id': 'sensor.roborock_q10_s5_sensor_remaining_life', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -779,36 +743,36 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Cleaning area', + 'object_id_base': 'Sensor remaining life', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cleaning area', + 'original_name': 'Sensor remaining life', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cleaning_area', - 'unique_id': 'cleaning_area_q10_duid', - 'unit_of_measurement': , + 'translation_key': 'sensor_life', + 'unique_id': 'sensor_life_q10_duid', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_cleaning_area-state] +# name: test_sensors[sensor.roborock_q10_s5_sensor_remaining_life-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+ Cleaning area', - 'unit_of_measurement': , + 'friendly_name': 'Roborock Q10 S5+ Sensor remaining life', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_cleaning_area', + 'entity_id': 'sensor.roborock_q10_s5_sensor_remaining_life', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '15', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_cleaning_progress-entry] +# name: test_sensors[sensor.roborock_q10_s5_side_brush_remaining_life-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -822,7 +786,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_cleaning_progress', + 'entity_id': 'sensor.roborock_q10_s5_side_brush_remaining_life', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -830,42 +794,64 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Cleaning progress', + 'object_id_base': 'Side brush remaining life', 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cleaning progress', + 'original_name': 'Side brush remaining life', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'clean_percent', - 'unique_id': 'clean_percent_q10_duid', + 'translation_key': 'side_brush_life', + 'unique_id': 'side_brush_life_q10_duid', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_cleaning_progress-state] +# name: test_sensors[sensor.roborock_q10_s5_side_brush_remaining_life-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+ Cleaning progress', + 'friendly_name': 'Roborock Q10 S5+ Side brush remaining life', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_cleaning_progress', + 'entity_id': 'sensor.roborock_q10_s5_side_brush_remaining_life', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_cleaning_time-entry] +# name: test_sensors[sensor.roborock_q10_s5_status-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'unknown', + 'sleeping', + 'idle', + 'cleaning', + 'returning_home', + 'remote_control_active', + 'charging', + 'paused', + 'error', + 'updating', + 'emptying_the_bin', + 'mapping', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', + ]), + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -873,7 +859,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', + 'entity_id': 'sensor.roborock_q10_s5_status', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -881,40 +867,54 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Cleaning time', + 'object_id_base': 'Status', 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Cleaning time', + 'original_name': 'Status', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cleaning_time', - 'unique_id': 'cleaning_time_q10_duid', - 'unit_of_measurement': , + 'translation_key': 'q10_status', + 'unique_id': 'status_q10_duid', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.roborock_q10_s5_cleaning_time-state] +# name: test_sensors[sensor.roborock_q10_s5_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Roborock Q10 S5+ Cleaning time', - 'unit_of_measurement': , + 'device_class': 'enum', + 'friendly_name': 'Roborock Q10 S5+ Status', + 'options': list([ + 'unknown', + 'sleeping', + 'idle', + 'cleaning', + 'returning_home', + 'remote_control_active', + 'charging', + 'paused', + 'error', + 'updating', + 'emptying_the_bin', + 'mapping', + 'saving_map', + 'relocating', + 'sweeping', + 'mopping', + 'sweep_and_mop', + 'transitioning', + 'waiting_to_charge', + ]), }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', + 'entity_id': 'sensor.roborock_q10_s5_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.0', + 'state': 'charging', }) # --- # name: test_sensors[sensor.roborock_q10_s5_total_cleaning_area-entry] From af650bc128bb329ab447cd034237816d703ba590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:21:40 +0200 Subject: [PATCH 46/54] Remove Q10 vacuum_error sensor (to be added in follow-up PR) The B01 fault code enum lives in the Q7-specific module with no shared export path, making a correct implementation non-trivial. Remove the incomplete sensor, its snapshot entries, and its test so the PR stays clean; a dedicated PR will add it properly. --- homeassistant/components/roborock/sensor.py | 8 - tests/components/roborock/conftest.py | 2 - .../roborock/snapshots/test_sensor.ambr | 162 ------------------ tests/components/roborock/test_sensor.py | 26 +-- 4 files changed, 1 insertion(+), 197 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 2c66dd9beef4e7..3e89232cccf4a3 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -515,14 +515,6 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ), - RoborockSensorDescriptionQ10( - key="vacuum_error", - translation_key="vacuum_error", - value_fn=lambda status: status.fault, - device_class=SensorDeviceClass.ENUM, - options=RoborockErrorCode.keys(), - entity_category=EntityCategory.DIAGNOSTIC, - ), ] diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index e5a3668d0693ea..6edbcb6f639d22 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -212,8 +212,6 @@ async def refresh_side_effect() -> None: dps[B01_Q10_DP.NOT_DISTURB] = _raw_value(status.not_disturb) if hasattr(status, "dust_switch"): dps[B01_Q10_DP.DUST_SWITCH] = _raw_value(status.dust_switch) - if hasattr(status, "fault"): - dps[B01_Q10_DP.FAULT] = _raw_value(status.fault) status.update_from_dps(dps) q10_trait.refresh = AsyncMock(side_effect=refresh_side_effect) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index c02245d7aab223..59864c3f801f79 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -1085,168 +1085,6 @@ 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_vacuum_error-entry] - EntityRegistryEntrySnapshot({ - 'aliases': list([ - None, - ]), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'none', - 'lidar_blocked', - 'bumper_stuck', - 'wheels_suspended', - 'cliff_sensor_error', - 'main_brush_jammed', - 'side_brush_jammed', - 'wheels_jammed', - 'robot_trapped', - 'no_dustbin', - 'strainer_error', - 'compass_error', - 'low_battery', - 'charging_error', - 'battery_error', - 'wall_sensor_dirty', - 'robot_tilted', - 'side_brush_error', - 'fan_error', - 'dock', - 'optical_flow_sensor_dirt', - 'vertical_bumper_pressed', - 'dock_locator_error', - 'return_to_dock_fail', - 'nogo_zone_detected', - 'visual_sensor', - 'light_touch', - 'vibrarise_jammed', - 'robot_on_carpet', - 'filter_blocked', - 'invisible_wall_detected', - 'cannot_cross_carpet', - 'internal_error', - 'collect_dust_error_3', - 'collect_dust_error_4', - 'mopping_roller_1', - 'mopping_roller_error_2', - 'clear_water_box_hoare', - 'dirty_water_box_hoare', - 'sink_strainer_hoare', - 'clear_water_box_exception', - 'clear_brush_exception', - 'clear_brush_exception_2', - 'filter_screen_exception', - 'mopping_roller_2', - 'up_water_exception', - 'drain_water_exception', - 'temperature_protection', - 'clean_carousel_exception', - 'clean_carousel_water_full', - 'water_carriage_drop', - 'check_clean_carouse', - 'audio_error', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_vacuum_error', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'object_id_base': 'Vacuum error', - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Vacuum error', - 'platform': 'roborock', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'vacuum_error', - 'unique_id': 'vacuum_error_q10_duid', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.roborock_q10_s5_vacuum_error-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Roborock Q10 S5+ Vacuum error', - 'options': list([ - 'none', - 'lidar_blocked', - 'bumper_stuck', - 'wheels_suspended', - 'cliff_sensor_error', - 'main_brush_jammed', - 'side_brush_jammed', - 'wheels_jammed', - 'robot_trapped', - 'no_dustbin', - 'strainer_error', - 'compass_error', - 'low_battery', - 'charging_error', - 'battery_error', - 'wall_sensor_dirty', - 'robot_tilted', - 'side_brush_error', - 'fan_error', - 'dock', - 'optical_flow_sensor_dirt', - 'vertical_bumper_pressed', - 'dock_locator_error', - 'return_to_dock_fail', - 'nogo_zone_detected', - 'visual_sensor', - 'light_touch', - 'vibrarise_jammed', - 'robot_on_carpet', - 'filter_blocked', - 'invisible_wall_detected', - 'cannot_cross_carpet', - 'internal_error', - 'collect_dust_error_3', - 'collect_dust_error_4', - 'mopping_roller_1', - 'mopping_roller_error_2', - 'clear_water_box_hoare', - 'dirty_water_box_hoare', - 'sink_strainer_hoare', - 'clear_water_box_exception', - 'clear_brush_exception', - 'clear_brush_exception_2', - 'filter_screen_exception', - 'mopping_roller_2', - 'up_water_exception', - 'drain_water_exception', - 'temperature_protection', - 'clean_carousel_exception', - 'clean_carousel_water_full', - 'water_carriage_drop', - 'check_clean_carouse', - 'audio_error', - ]), - }), - 'context': , - 'entity_id': 'sensor.roborock_q10_s5_vacuum_error', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensors[sensor.roborock_q7_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 15ad1c69de537b..7b14fec6204215 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -3,12 +3,10 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import FakeDevice - from tests.common import MockConfigEntry, snapshot_platform @@ -26,25 +24,3 @@ async def test_sensors( ) -> None: """Test sensors and check test values are correctly set.""" await snapshot_platform(hass, entity_registry, snapshot, setup_entry.entry_id) - - -async def test_q10_vacuum_error_updates_from_push( - hass: HomeAssistant, - setup_entry: MockConfigEntry, - fake_q10_vacuum: FakeDevice, -) -> None: - """Test Q10 vacuum error sensor updates when status trait pushes updates.""" - entity_id = "sensor.roborock_q10_s5_vacuum_error" - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNKNOWN - - assert fake_q10_vacuum.b01_q10_properties is not None - # Mutate the fault value, then call refresh to simulate a device push - fake_q10_vacuum.b01_q10_properties.status.fault = 5 - await fake_q10_vacuum.b01_q10_properties.refresh() - await hass.async_block_till_done() - - updated_state = hass.states.get(entity_id) - assert updated_state is not None - assert updated_state.state == "unknown" From 6fb2336feddcb0c3f886a5fa8c8b9da3a11db044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:46:23 +0200 Subject: [PATCH 47/54] Add Q10 status icon to roborock component --- homeassistant/components/roborock/icons.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 6a3707428db219..5bfeb8ac1a6388 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -85,6 +85,9 @@ "mop_life_time_left": { "default": "mdi:texture" }, + "q10_status": { + "default": "mdi:information-outline" + }, "q7_status": { "default": "mdi:information-outline" }, From 82d8778d65de812e6563f785d0eb108e1c1d7435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:39:23 +0200 Subject: [PATCH 48/54] Simplify Q10 refresh mock to a no-op AsyncMock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous refresh_side_effect did a circular DPS round-trip (reading existing properties, converting to DPS, then re-applying the same values) that served no purpose — no test asserted any state change from it. --- tests/components/roborock/conftest.py | 33 +-------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 6edbcb6f639d22..bfbf1ff5a4f004 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -33,7 +33,6 @@ ZeoError, ZeoState, ) -from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP from roborock.devices.device import RoborockDevice from roborock.devices.device_manager import DeviceManager from roborock.devices.traits.b01.q10.status import StatusTrait as Q10StatusTrait @@ -184,37 +183,7 @@ def create_b01_q10_trait() -> Mock: q10_trait.vacuum = AsyncMock() q10_trait.command = AsyncMock() - def _raw_value(value: Any) -> Any: - return ( - value.code - if hasattr(value, "code") - else value.value - if hasattr(value, "value") - else value - ) - - async def refresh_side_effect() -> None: - """Simulate a device push via the public Q10 DPS update API with new data.""" - # Mutate clean_count to simulate a new cleaning cycle - status.clean_count += 1 - dps: dict[B01_Q10_DP, Any] = { - B01_Q10_DP.STATUS: _raw_value(status.status), - B01_Q10_DP.BATTERY: status.battery, - B01_Q10_DP.FAN_LEVEL: _raw_value(status.fan_level), - B01_Q10_DP.WATER_LEVEL: _raw_value(status.water_level), - B01_Q10_DP.CLEAN_COUNT: status.clean_count, - B01_Q10_DP.CLEAN_TIME: status.clean_time, - B01_Q10_DP.CLEAN_AREA: status.clean_area, - } - if hasattr(status, "child_lock"): - dps[B01_Q10_DP.CHILD_LOCK] = _raw_value(status.child_lock) - if hasattr(status, "not_disturb"): - dps[B01_Q10_DP.NOT_DISTURB] = _raw_value(status.not_disturb) - if hasattr(status, "dust_switch"): - dps[B01_Q10_DP.DUST_SWITCH] = _raw_value(status.dust_switch) - status.update_from_dps(dps) - - q10_trait.refresh = AsyncMock(side_effect=refresh_side_effect) + q10_trait.refresh = AsyncMock() return q10_trait From 82ed84a85e2cd6f14692f954cf7d33dddf470a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:47:26 +0200 Subject: [PATCH 49/54] Reuse status translation key for Q10 status sensor All Q10 status states are a subset of those already defined in the `status` translation key, so there's no need for a separate `q10_status` key in strings.json and icons.json. --- homeassistant/components/roborock/icons.json | 3 --- homeassistant/components/roborock/sensor.py | 2 +- .../components/roborock/strings.json | 24 ------------------- .../roborock/snapshots/test_sensor.ambr | 2 +- 4 files changed, 2 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 5bfeb8ac1a6388..6a3707428db219 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -85,9 +85,6 @@ "mop_life_time_left": { "default": "mdi:texture" }, - "q10_status": { - "default": "mdi:information-outline" - }, "q7_status": { "default": "mdi:information-outline" }, diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 3e89232cccf4a3..8a9e34ed55929d 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -426,7 +426,7 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: Q10_B01_SENSOR_DESCRIPTIONS = [ RoborockSensorDescriptionQ10( key="status", - translation_key="q10_status", + translation_key="status", device_class=SensorDeviceClass.ENUM, value_fn=lambda data: data.status.value if data.status is not None else None, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index d6f8aeaadbb3c2..50c3c5298e9819 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -392,30 +392,6 @@ "mop_life_time_left": { "name": "Mop life time left" }, - "q10_status": { - "name": "Status", - "state": { - "charging": "[%key:common::state::charging%]", - "cleaning": "[%key:component::roborock::entity::sensor::status::state::cleaning%]", - "emptying_the_bin": "[%key:component::roborock::entity::sensor::status::state::emptying_the_bin%]", - "error": "[%key:common::state::error%]", - "idle": "[%key:common::state::idle%]", - "mapping": "[%key:component::roborock::entity::sensor::status::state::mapping%]", - "mopping": "[%key:component::roborock::entity::sensor::status::state::mopping%]", - "paused": "[%key:common::state::paused%]", - "relocating": "[%key:component::roborock::entity::sensor::status::state::relocating%]", - "remote_control_active": "[%key:component::roborock::entity::sensor::status::state::remote_control_active%]", - "returning_home": "[%key:component::roborock::entity::sensor::status::state::returning_home%]", - "saving_map": "[%key:component::roborock::entity::sensor::status::state::saving_map%]", - "sleeping": "[%key:component::roborock::entity::sensor::status::state::sleeping%]", - "sweep_and_mop": "[%key:component::roborock::entity::sensor::status::state::sweep_and_mop%]", - "sweeping": "[%key:component::roborock::entity::sensor::status::state::sweeping%]", - "transitioning": "[%key:component::roborock::entity::sensor::status::state::transitioning%]", - "unknown": "[%key:component::roborock::entity::sensor::status::state::unknown%]", - "updating": "[%key:component::roborock::entity::sensor::status::state::updating%]", - "waiting_to_charge": "[%key:component::roborock::entity::sensor::status::state::waiting_to_charge%]" - } - }, "q7_status": { "name": "Status", "state": { diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 59864c3f801f79..531fbf93897fc8 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -877,7 +877,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'q10_status', + 'translation_key': 'status', 'unique_id': 'status_q10_duid', 'unit_of_measurement': None, }) From 27814287c92b7f17078000b623dd1981c589a10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:05:38 +0200 Subject: [PATCH 50/54] Fix Q10 consumable sensor units from percentage to hours The *_life sensors (main_brush_life, side_brush_life, filter_life, sensor_life) report hours of usage, not a percentage. Update units to UnitOfTime.HOURS with device_class DURATION, fix misleading names in strings.json, and remove explicit icons so HA uses the DURATION default (mdi:timer) to visually distinguish them from *_time_left sensors. --- homeassistant/components/roborock/icons.json | 12 --- homeassistant/components/roborock/sensor.py | 12 ++- .../components/roborock/strings.json | 8 +- .../roborock/snapshots/test_sensor.ambr | 96 +++++++++++-------- 4 files changed, 68 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 6a3707428db219..de90a4133380b9 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -61,9 +61,6 @@ "dock_error": { "default": "mdi:garage-open" }, - "filter_life": { - "default": "mdi:air-filter" - }, "filter_time_left": { "default": "mdi:air-filter" }, @@ -73,9 +70,6 @@ "last_clean_start": { "default": "mdi:clock-time-twelve" }, - "main_brush_life": { - "default": "mdi:brush" - }, "main_brush_time_left": { "default": "mdi:brush" }, @@ -88,15 +82,9 @@ "q7_status": { "default": "mdi:information-outline" }, - "sensor_life": { - "default": "mdi:eye-outline" - }, "sensor_time_left": { "default": "mdi:eye-outline" }, - "side_brush_life": { - "default": "mdi:brush" - }, "side_brush_time_left": { "default": "mdi:brush" }, diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 8a9e34ed55929d..fa1102905613ac 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -485,28 +485,32 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: translation_key="main_brush_life", value_fn=lambda data: data.main_brush_life, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, ), RoborockSensorDescriptionQ10( key="side_brush_life", translation_key="side_brush_life", value_fn=lambda data: data.side_brush_life, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, ), RoborockSensorDescriptionQ10( key="filter_life", translation_key="filter_life", value_fn=lambda data: data.filter_life, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, ), RoborockSensorDescriptionQ10( key="sensor_life", translation_key="sensor_life", value_fn=lambda data: data.sensor_life, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, ), RoborockSensorDescriptionQ10( key="clean_percent", diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 50c3c5298e9819..d23bac00324857 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -369,7 +369,7 @@ } }, "filter_life": { - "name": "Filter remaining life" + "name": "Filter time used" }, "filter_time_left": { "name": "Filter time left" @@ -381,7 +381,7 @@ "name": "Last clean begin" }, "main_brush_life": { - "name": "Main brush remaining life" + "name": "Main brush time used" }, "main_brush_time_left": { "name": "Main brush time left" @@ -409,13 +409,13 @@ } }, "sensor_life": { - "name": "Sensor remaining life" + "name": "Sensor time used" }, "sensor_time_left": { "name": "Sensor time left" }, "side_brush_life": { - "name": "Side brush remaining life" + "name": "Side brush time used" }, "side_brush_time_left": { "name": "Side brush time left" diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 531fbf93897fc8..c08fdb6f91fd33 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -619,7 +619,7 @@ 'state': '2.0', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_filter_remaining_life-entry] +# name: test_sensors[sensor.roborock_q10_s5_filter_time_used-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -633,7 +633,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_filter_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_filter_time_used', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -641,36 +641,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Filter remaining life', + 'object_id_base': 'Filter time used', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Filter remaining life', + 'original_name': 'Filter time used', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_life', 'unique_id': 'filter_life_q10_duid', - 'unit_of_measurement': '%', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.roborock_q10_s5_filter_remaining_life-state] +# name: test_sensors[sensor.roborock_q10_s5_filter_time_used-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+ Filter remaining life', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Filter time used', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_filter_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_filter_time_used', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_main_brush_remaining_life-entry] +# name: test_sensors[sensor.roborock_q10_s5_main_brush_time_used-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -684,7 +688,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_main_brush_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_main_brush_time_used', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -692,36 +696,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Main brush remaining life', + 'object_id_base': 'Main brush time used', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Main brush remaining life', + 'original_name': 'Main brush time used', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'main_brush_life', 'unique_id': 'main_brush_life_q10_duid', - 'unit_of_measurement': '%', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.roborock_q10_s5_main_brush_remaining_life-state] +# name: test_sensors[sensor.roborock_q10_s5_main_brush_time_used-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+ Main brush remaining life', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Main brush time used', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_main_brush_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_main_brush_time_used', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_sensor_remaining_life-entry] +# name: test_sensors[sensor.roborock_q10_s5_sensor_time_used-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -735,7 +743,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_sensor_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_sensor_time_used', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -743,36 +751,40 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Sensor remaining life', + 'object_id_base': 'Sensor time used', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Sensor remaining life', + 'original_name': 'Sensor time used', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensor_life', 'unique_id': 'sensor_life_q10_duid', - 'unit_of_measurement': '%', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.roborock_q10_s5_sensor_remaining_life-state] +# name: test_sensors[sensor.roborock_q10_s5_sensor_time_used-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+ Sensor remaining life', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Sensor time used', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_sensor_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_sensor_time_used', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_sensors[sensor.roborock_q10_s5_side_brush_remaining_life-entry] +# name: test_sensors[sensor.roborock_q10_s5_side_brush_time_used-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -786,7 +798,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.roborock_q10_s5_side_brush_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_side_brush_time_used', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -794,29 +806,33 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Side brush remaining life', + 'object_id_base': 'Side brush time used', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Side brush remaining life', + 'original_name': 'Side brush time used', 'platform': 'roborock', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'side_brush_life', 'unique_id': 'side_brush_life_q10_duid', - 'unit_of_measurement': '%', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.roborock_q10_s5_side_brush_remaining_life-state] +# name: test_sensors[sensor.roborock_q10_s5_side_brush_time_used-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Roborock Q10 S5+ Side brush remaining life', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Side brush time used', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.roborock_q10_s5_side_brush_remaining_life', + 'entity_id': 'sensor.roborock_q10_s5_side_brush_time_used', 'last_changed': , 'last_reported': , 'last_updated': , From 0ed749692d12db35d405dbc709e713c7c343878e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:07:57 +0200 Subject: [PATCH 51/54] Add consumable hours to Q10 mock data and update snapshots Add realistic mock values (in hours) for main_brush_life, side_brush_life, filter_life and sensor_life matching real device data, so tests validate actual sensor states instead of unknown. --- tests/components/roborock/mock_data.py | 4 ++++ tests/components/roborock/snapshots/test_sensor.ambr | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index f55c6dbec30f83..ccb9bdbd5eb49e 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1575,4 +1575,8 @@ fan_level=YXFanLevel.BALANCED, water_level=YXWaterLevel.MEDIUM, clean_count=1, + main_brush_life=81, + side_brush_life=90, + filter_life=90, + sensor_life=28, ) diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index c08fdb6f91fd33..761b132e85c717 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -671,7 +671,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '90', }) # --- # name: test_sensors[sensor.roborock_q10_s5_main_brush_time_used-entry] @@ -726,7 +726,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '81', }) # --- # name: test_sensors[sensor.roborock_q10_s5_sensor_time_used-entry] @@ -781,7 +781,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '28', }) # --- # name: test_sensors[sensor.roborock_q10_s5_side_brush_time_used-entry] @@ -836,7 +836,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '90', }) # --- # name: test_sensors[sensor.roborock_q10_s5_status-entry] From bd1af388bbb03c2e92acff6bc5c52b857385d634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:11:22 +0200 Subject: [PATCH 52/54] Fix Q10 cleaning_time unit from minutes to seconds CLEAN_TIME is reported in seconds by the device (1812 = ~30 min). Update native unit to seconds with suggested display in minutes. Update mock value to 1800 s (30 min) to match real device data. --- homeassistant/components/roborock/sensor.py | 4 ++-- tests/components/roborock/mock_data.py | 2 +- tests/components/roborock/snapshots/test_sensor.ambr | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index fa1102905613ac..467aa47bcb1210 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -444,8 +444,8 @@ def _dock_error_value_fn(state: DeviceState) -> str | None: translation_key="cleaning_time", value_fn=lambda data: data.clean_time, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=UnitOfTime.MINUTES, - suggested_unit_of_measurement=UnitOfTime.HOURS, + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, ), RoborockSensorDescriptionQ10( diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index ccb9bdbd5eb49e..b62697fffc1d6f 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1568,7 +1568,7 @@ ) Q10_STATUS = Q10Status( - clean_time=120, + clean_time=1800, clean_area=15, battery=100, status=YXDeviceState.CHARGING, diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 761b132e85c717..541b94addd0d44 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -589,7 +589,7 @@ 'suggested_display_precision': 2, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -601,7 +601,7 @@ 'supported_features': 0, 'translation_key': 'cleaning_time', 'unique_id': 'cleaning_time_q10_duid', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.roborock_q10_s5_cleaning_time-state] @@ -609,14 +609,14 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'Roborock Q10 S5+ Cleaning time', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.roborock_q10_s5_cleaning_time', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.0', + 'state': '30.0', }) # --- # name: test_sensors[sensor.roborock_q10_s5_filter_time_used-entry] From dae51e9d0f651a06db0c3eecb2345589b393da89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:29:15 +0200 Subject: [PATCH 53/54] Remove unnecessary blank line in create_b01_q10_trait function --- tests/components/roborock/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index bfbf1ff5a4f004..d574d495f8a2da 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -182,7 +182,6 @@ def create_b01_q10_trait() -> Mock: q10_trait.vacuum = AsyncMock() q10_trait.command = AsyncMock() - q10_trait.refresh = AsyncMock() return q10_trait From a66f0835021621dbcebb6eb655aaf3c73727f8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <132135057+lboue@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:58:49 +0200 Subject: [PATCH 54/54] Remove q7_status icon from icons.json --- homeassistant/components/roborock/icons.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index de90a4133380b9..e5c1c6e208184d 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -79,9 +79,6 @@ "mop_life_time_left": { "default": "mdi:texture" }, - "q7_status": { - "default": "mdi:information-outline" - }, "sensor_time_left": { "default": "mdi:eye-outline" },