Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
d310d65
Add Roborock Q10 sensor entities and snapshots
lboue Mar 21, 2026
2be4358
Add additional states for Roborock vacuum in strings.json
lboue Mar 21, 2026
ea847ab
Add new states for Roborock vacuum in strings.json
lboue Mar 21, 2026
21c0651
feat(roborock): add consumable life and total stats sensors for Q10
lboue Mar 21, 2026
b7caf32
Add missing icons for roborock sensors
lboue Mar 21, 2026
1503967
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Mar 21, 2026
95b273c
Adds eye icon for the sensor remaining life entity
lboue Mar 21, 2026
5f42242
Refine Q10 coordinator setup behavior
lboue Mar 21, 2026
8c6ceb6
Remove sensor_remaining_life icon entry from icons.json
lboue Mar 21, 2026
dd6bd9d
Refactor comment for device push simulation in Q10 trait tests
lboue Mar 21, 2026
90aa85c
Update comment for sensor registration in Q10 setup to clarify state …
lboue Mar 21, 2026
1617ef0
Align roborock icon key with mop drying translation key
lboue Mar 21, 2026
68f2bc8
Remove redundant comments regarding Q10 sensor registration process
lboue Mar 21, 2026
3e5a29e
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Mar 21, 2026
0ede1fe
Adds eye icon for the sensor remaining life entity
lboue Mar 21, 2026
0975670
Fix Q10 vacuum_error sensor support
lboue Mar 21, 2026
39b1ca0
Add test for Q10 vacuum error sensor updates from push notifications
lboue Mar 21, 2026
a450898
Implement Q10 vacuum refresh simulation using DPS update API
lboue Mar 21, 2026
dbf2d5d
Add Q10 vacuum error handling and update error messages
lboue Mar 21, 2026
7496655
Remove battery sensor icon from Q10 vacuum icons configuration
lboue Mar 21, 2026
160d27e
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Mar 22, 2026
531d97d
Refactor Roborock state strings by removing unused keys for cleaner code
lboue Mar 22, 2026
291b27c
Refactor Q10 vacuum error test to use update_from_dps for status updates
lboue Mar 22, 2026
43a69e8
Remove unused sensor_remaining_life icon from Roborock icons.json
lboue Mar 22, 2026
3689ccf
Add new vacuum state strings for improved user feedback
lboue Mar 22, 2026
8a81bf0
tests: update roborock sensor snapshots with normalized YXDeviceState…
lboue Mar 22, 2026
2ea8495
roborock: add dedicated q10_status translation key for YXDeviceState …
lboue Mar 22, 2026
8bcd3e2
roborock: handle unknown Q10 error codes gracefully in _q10_error_val…
lboue Mar 22, 2026
41b852a
roborock: also catch TypeError in _q10_error_value_fn for unknown Q10…
lboue Mar 22, 2026
c117395
Add status sensor for Roborock Q10 S5+ with state options
lboue Mar 22, 2026
6b07457
bump python-roborock requirement to version 5.0.0
lboue Mar 22, 2026
9a49cc9
Update Q10 state mappings and mock data for Roborock vacuum integration
lboue Mar 22, 2026
161e520
Remove deprecated sensor entries and update state options for Roboroc…
lboue Mar 22, 2026
9dec2e7
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Mar 22, 2026
1e10fc0
Update Roborock vacuum status messages for improved clarity and consi…
lboue Mar 22, 2026
71d9704
Refactor Roborock vacuum status strings for consistency and clarity
lboue Mar 22, 2026
752c10e
Refactor Roborock vacuum status strings for consistency and clarity
lboue Mar 23, 2026
8540e98
Refactor Roborock vacuum status strings for consistency and clarity
lboue Mar 23, 2026
d48a82b
Refactor Roborock vacuum status strings for consistency and clarity
lboue Mar 23, 2026
c290c91
Enhance Q10 DPS update API simulation with new data handling
lboue Mar 23, 2026
95572be
Update Q10 vacuum error test to simulate device push with fault mutation
lboue Mar 23, 2026
15fbb5e
Fix exception handling in Q10 error value function
lboue Mar 23, 2026
240c3b7
Add standby state string for Roborock vacuum
lboue Mar 23, 2026
5215f49
Remove unused Q10 error value function to clean up code
lboue Mar 23, 2026
05d05f6
Update Q10 vacuum error value function to return fault status and adj…
lboue Mar 23, 2026
2f05f5e
Remove unused import from Q10 vacuum sensor test
lboue Mar 23, 2026
25606aa
Remove unnecessary blank lines in Roborock sensor description
lboue Mar 23, 2026
a15f102
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Apr 3, 2026
febc0f7
Include FAULT DP in roborock Q10 refresh push simulation
Luligu Apr 3, 2026
6ecdd9b
Update roborock sensor snapshots for Q10 S5+ entities
Luligu Apr 3, 2026
af650bc
Remove Q10 vacuum_error sensor (to be added in follow-up PR)
Luligu Apr 3, 2026
6fb2336
Add Q10 status icon to roborock component
Luligu Apr 3, 2026
82d8778
Simplify Q10 refresh mock to a no-op AsyncMock
Luligu Apr 6, 2026
82ed84a
Reuse status translation key for Q10 status sensor
Luligu Apr 6, 2026
2781428
Fix Q10 consumable sensor units from percentage to hours
Luligu Apr 6, 2026
0ed7496
Add consumable hours to Q10 mock data and update snapshots
Luligu Apr 6, 2026
bd1af38
Fix Q10 cleaning_time unit from minutes to seconds
Luligu Apr 6, 2026
6653f70
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Apr 6, 2026
dae51e9
Remove unnecessary blank line in create_b01_q10_trait function
Luligu Apr 6, 2026
a66f083
Remove q7_status icon from icons.json
Luligu Apr 6, 2026
53f6d78
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-sensors
lboue Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions homeassistant/components/roborock/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
lboue marked this conversation as resolved.
"""
try:
await self.api.refresh()
Comment thread
lboue marked this conversation as resolved.
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/roborock/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
}
},
"sensor": {
"brush_remaining": {
"default": "mdi:brush"
},
"clean_percent": {
"default": "mdi:progress-check"
},
Expand All @@ -49,6 +52,9 @@
"cleaning_brush_time_left": {
"default": "mdi:brush"
},
"cleaning_time": {
"default": "mdi:clock-outline"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you decide between mdi:brush vs mdi:clock-outline here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duration of the last cleaning depends on the vacuum cleaner as a whole, not just the brush.

},
"countdown": {
"default": "mdi:clock-outline"
},
Expand All @@ -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"
},
Expand All @@ -79,6 +91,9 @@
"strainer_time_left": {
"default": "mdi:filter-variant"
},
"times_after_clean": {
"default": "mdi:counter"
},
"total_cleaning_area": {
"default": "mdi:texture-box"
},
Expand Down
142 changes: 142 additions & 0 deletions homeassistant/components/roborock/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -34,6 +36,7 @@

from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockB01Q10UpdateCoordinator,
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
Expand All @@ -43,6 +46,7 @@
from .entity import (
RoborockCoordinatedEntityA01,
RoborockCoordinatedEntityB01Q7,
RoborockCoordinatedEntityB01Q10,
RoborockCoordinatedEntityV1,
RoborockEntity,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
),
Comment thread
lboue marked this conversation as resolved.
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,
),
Comment thread
lboue marked this conversation as resolved.
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,
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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)
28 changes: 28 additions & 0 deletions homeassistant/components/roborock/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@
"water_empty": "Water empty"
}
},
"filter_life": {
"name": "Filter time used"
},
"filter_time_left": {
"name": "Filter time left"
},
Expand All @@ -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"
},
Expand All @@ -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"
},
Expand All @@ -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"
}
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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"
}
Expand Down
6 changes: 5 additions & 1 deletion tests/components/roborock/mock_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Loading
Loading