diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 146ba9653651ea..645cbbea0c39ac 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -608,8 +608,9 @@ def __init__( 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. + 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: await self.api.refresh() diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index f6053090bb7a97..e5c1c6e208184d 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -40,6 +40,9 @@ } }, "sensor": { + "brush_remaining": { + "default": "mdi:brush" + }, "clean_percent": { "default": "mdi:progress-check" }, @@ -49,6 +52,9 @@ "cleaning_brush_time_left": { "default": "mdi:brush" }, + "cleaning_time": { + "default": "mdi:clock-outline" + }, "countdown": { "default": "mdi:clock-outline" }, @@ -67,6 +73,12 @@ "main_brush_time_left": { "default": "mdi:brush" }, + "mop_drying_remaining_time": { + "default": "mdi:clock-outline" + }, + "mop_life_time_left": { + "default": "mdi:texture" + }, "sensor_time_left": { "default": "mdi:eye-outline" }, @@ -79,6 +91,9 @@ "strainer_time_left": { "default": "mdi:filter-variant" }, + "times_after_clean": { + "default": "mdi:counter" + }, "total_cleaning_area": { "default": "mdi:texture-box" }, diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 48f5407f86cee1..467aa47bcb1210 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 @@ -412,6 +423,105 @@ 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.SECONDS, + suggested_unit_of_measurement=UnitOfTime.MINUTES, + 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.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=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=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=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=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, + ), + RoborockSensorDescriptionQ10( + key="clean_percent", + translation_key="clean_percent", + value_fn=lambda data: data.cleaning_progress, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: RoborockConfigEntry, @@ -460,6 +570,11 @@ 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 + ) async_add_entities(entities) @@ -568,3 +683,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/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index e3ba066f9ba9a2..d23bac00324857 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -368,6 +368,9 @@ "water_empty": "Water empty" } }, + "filter_life": { + "name": "Filter time used" + }, "filter_time_left": { "name": "Filter time left" }, @@ -377,6 +380,9 @@ "last_clean_start": { "name": "Last clean begin" }, + "main_brush_life": { + "name": "Main brush time used" + }, "main_brush_time_left": { "name": "Main brush time left" }, @@ -402,9 +408,15 @@ "waiting_for_orders": "Waiting for orders" } }, + "sensor_life": { + "name": "Sensor time used" + }, "sensor_time_left": { "name": "Sensor time left" }, + "side_brush_life": { + "name": "Side brush time used" + }, "side_brush_time_left": { "name": "Side brush time left" }, @@ -430,15 +442,23 @@ "locked": "Locked", "manual_mode": "Manual mode", "mapping": "Mapping", + "mopping": "Mopping", "paused": "[%key:common::state::paused%]", + "relocating": "Relocating", "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", "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" } @@ -461,10 +481,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", @@ -476,6 +500,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", @@ -491,6 +516,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", @@ -500,10 +526,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/mock_data.py b/tests/components/roborock/mock_data.py index f55c6dbec30f83..b62697fffc1d6f 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1568,11 +1568,15 @@ ) Q10_STATUS = Q10Status( - clean_time=120, + clean_time=1800, clean_area=15, battery=100, status=YXDeviceState.CHARGING, 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 0d9b0829ccaa67..541b94addd0d44 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -407,6 +407,700 @@ '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_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([ + 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': '30.0', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_filter_time_used-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_time_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Filter time used', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + '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': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_filter_time_used-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Filter time used', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_filter_time_used', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_main_brush_time_used-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_time_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main brush time used', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + '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': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_main_brush_time_used-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Main brush time used', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_main_brush_time_used', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '81', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_sensor_time_used-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_time_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Sensor time used', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + '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': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_sensor_time_used-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Sensor time used', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_sensor_time_used', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_side_brush_time_used-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_time_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Side brush time used', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + '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': , + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_side_brush_time_used-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q10 S5+ Side brush time used', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q10_s5_side_brush_time_used', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_sensors[sensor.roborock_q10_s5_status-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', + ]), + }), + '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', + '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_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# 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([ + 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': '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_battery-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+", } diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 953c390b8e148c..80db9ae931b8e2 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -1012,14 +1012,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" @@ -1028,7 +1028,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)