Add basic support for Q7 devices (#159274)

This commit is contained in:
Luke Lashley
2025-12-18 06:30:20 -05:00
committed by GitHub
parent 4960871c84
commit 5349045932
10 changed files with 956 additions and 10 deletions

View File

@@ -37,10 +37,12 @@ from .const import (
PLATFORMS,
)
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockConfigEntry,
RoborockCoordinators,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
RoborockDataUpdateCoordinatorB01,
RoborockWashingMachineUpdateCoordinator,
RoborockWetDryVacUpdateCoordinator,
)
@@ -131,13 +133,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
for coord in coordinators
if isinstance(coord, RoborockDataUpdateCoordinatorA01)
]
if len(v1_coords) + len(a01_coords) == 0:
b01_coords = [
coord
for coord in coordinators
if isinstance(coord, RoborockDataUpdateCoordinatorB01)
]
if len(v1_coords) + len(a01_coords) + len(b01_coords) == 0:
raise ConfigEntryNotReady(
"No devices were able to successfully setup",
translation_domain=DOMAIN,
translation_key="no_coordinators",
)
entry.runtime_data = RoborockCoordinators(v1_coords, a01_coords)
entry.runtime_data = RoborockCoordinators(v1_coords, a01_coords, b01_coords)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -208,12 +215,17 @@ def build_setup_functions(
Coroutine[
Any,
Any,
RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None,
RoborockDataUpdateCoordinator
| RoborockDataUpdateCoordinatorA01
| RoborockDataUpdateCoordinatorB01
| None,
]
]:
"""Create a list of setup functions that can later be called asynchronously."""
coordinators: list[
RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01
RoborockDataUpdateCoordinator
| RoborockDataUpdateCoordinatorA01
| RoborockDataUpdateCoordinatorB01
] = []
for device in devices:
_LOGGER.debug("Creating device %s: %s", device.name, device)
@@ -229,6 +241,12 @@ def build_setup_functions(
coordinators.append(
RoborockWashingMachineUpdateCoordinator(hass, entry, device, device.zeo)
)
elif device.b01_q7_properties is not None:
coordinators.append(
RoborockB01Q7UpdateCoordinator(
hass, entry, device, device.b01_q7_properties
)
)
else:
_LOGGER.warning(
"Not adding device %s because its protocol version %s or category %s is not supported",
@@ -241,8 +259,15 @@ def build_setup_functions(
async def setup_coordinator(
coordinator: RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01,
) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None:
coordinator: RoborockDataUpdateCoordinator
| RoborockDataUpdateCoordinatorA01
| RoborockDataUpdateCoordinatorB01,
) -> (
RoborockDataUpdateCoordinator
| RoborockDataUpdateCoordinatorA01
| RoborockDataUpdateCoordinatorB01
| None
):
"""Set up a single coordinator."""
try:
await coordinator.async_config_entry_first_refresh()

View File

@@ -8,12 +8,18 @@ import logging
from typing import Any, TypeVar
from propcache.api import cached_property
from roborock import B01Props
from roborock.data import HomeDataScene
from roborock.devices.device import RoborockDevice
from roborock.devices.traits.a01 import DyadApi, ZeoApi
from roborock.devices.traits.b01 import Q7PropertiesApi
from roborock.devices.traits.v1 import PropertiesApi
from roborock.exceptions import RoborockDeviceBusy, RoborockException
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
from roborock.roborock_message import (
RoborockB01Props,
RoborockDyadDataProtocol,
RoborockZeoProtocol,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CONNECTIONS
@@ -58,12 +64,17 @@ class RoborockCoordinators:
v1: list[RoborockDataUpdateCoordinator]
a01: list[RoborockDataUpdateCoordinatorA01]
b01: list[RoborockDataUpdateCoordinatorB01]
def values(
self,
) -> list[RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01]:
) -> list[
RoborockDataUpdateCoordinator
| RoborockDataUpdateCoordinatorA01
| RoborockDataUpdateCoordinatorB01
]:
"""Return all coordinators."""
return self.v1 + self.a01
return self.v1 + self.a01 + self.b01
type RoborockConfigEntry = ConfigEntry[RoborockCoordinators]
@@ -469,3 +480,91 @@ class RoborockWetDryVacUpdateCoordinator(
translation_domain=DOMAIN,
translation_key="update_data_fail",
) from ex
class RoborockDataUpdateCoordinatorB01(DataUpdateCoordinator[B01Props]):
"""Class to manage fetching data from the API for B01 devices."""
config_entry: RoborockConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: RoborockConfigEntry,
device: RoborockDevice,
) -> None:
"""Initialize."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=A01_UPDATE_INTERVAL,
)
self._device = device
self.device_info = DeviceInfo(
name=device.name,
identifiers={(DOMAIN, device.duid)},
manufacturer="Roborock",
model=device.product.model,
sw_version=device.device_info.fv,
)
@cached_property
def duid(self) -> str:
"""Get the unique id of the device as specified by Roborock."""
return self._device.duid
@cached_property
def duid_slug(self) -> str:
"""Get the slug of the duid."""
return slugify(self.duid)
@property
def device(self) -> RoborockDevice:
"""Get the RoborockDevice."""
return self._device
class RoborockB01Q7UpdateCoordinator(RoborockDataUpdateCoordinatorB01):
"""Coordinator for B01 Q7 devices."""
def __init__(
self,
hass: HomeAssistant,
config_entry: RoborockConfigEntry,
device: RoborockDevice,
api: Q7PropertiesApi,
) -> None:
"""Initialize."""
super().__init__(hass, config_entry, device)
self.api = api
self.request_protocols: list[RoborockB01Props] = [
RoborockB01Props.STATUS,
RoborockB01Props.MAIN_BRUSH,
RoborockB01Props.SIDE_BRUSH,
RoborockB01Props.DUST_BAG_USED,
RoborockB01Props.MOP_LIFE,
RoborockB01Props.MAIN_SENSOR,
RoborockB01Props.CLEANING_TIME,
RoborockB01Props.REAL_CLEAN_TIME,
RoborockB01Props.HYPA,
]
async def _async_update_data(
self,
) -> B01Props:
try:
data = await self.api.query_values(self.request_protocols)
except RoborockException as ex:
_LOGGER.debug("Failed to update Q7 data: %s", ex)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_data_fail",
) from ex
if data is None:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_data_fail",
)
return data

View File

@@ -13,7 +13,11 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
from .coordinator import (
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
RoborockDataUpdateCoordinatorB01,
)
class RoborockEntity(Entity):
@@ -124,3 +128,23 @@ class RoborockCoordinatedEntityA01(
)
CoordinatorEntity.__init__(self, coordinator=coordinator)
self._attr_unique_id = unique_id
class RoborockCoordinatedEntityB01(
RoborockEntity, CoordinatorEntity[RoborockDataUpdateCoordinatorB01]
):
"""Representation of coordinated Roborock Entity."""
def __init__(
self,
unique_id: str,
coordinator: RoborockDataUpdateCoordinatorB01,
) -> None:
"""Initialize the coordinated Roborock Device."""
RoborockEntity.__init__(
self,
unique_id=unique_id,
device_info=coordinator.device_info,
)
CoordinatorEntity.__init__(self, coordinator=coordinator)
self._attr_unique_id = unique_id

View File

@@ -8,12 +8,14 @@ import datetime
import logging
from roborock.data import (
B01Props,
DyadError,
RoborockDockErrorCode,
RoborockDockTypeCode,
RoborockDyadStateCode,
RoborockErrorCode,
RoborockStateCode,
WorkStatusMapping,
ZeoError,
ZeoState,
)
@@ -34,9 +36,11 @@ from .coordinator import (
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
RoborockDataUpdateCoordinatorB01,
)
from .entity import (
RoborockCoordinatedEntityA01,
RoborockCoordinatedEntityB01,
RoborockCoordinatedEntityV1,
RoborockEntity,
)
@@ -64,6 +68,13 @@ class RoborockSensorDescriptionA01(SensorEntityDescription):
data_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol
@dataclass(frozen=True, kw_only=True)
class RoborockSensorDescriptionB01(SensorEntityDescription):
"""A class that describes Roborock B01 sensors."""
value_fn: Callable[[B01Props], StateType]
def _dock_error_value_fn(state: DeviceState) -> str | None:
if (
status := state.status.dock_error_status
@@ -326,6 +337,71 @@ A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [
),
]
Q7_B01_SENSOR_DESCRIPTIONS = [
RoborockSensorDescriptionB01(
key="q7_status",
value_fn=lambda data: data.status_name,
translation_key="q7_status",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=WorkStatusMapping.keys(),
),
RoborockSensorDescriptionB01(
key="main_brush_time_left",
value_fn=lambda data: data.main_brush_time_left,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
translation_key="main_brush_time_left",
entity_category=EntityCategory.DIAGNOSTIC,
),
RoborockSensorDescriptionB01(
key="side_brush_time_left",
value_fn=lambda data: data.side_brush_time_left,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
translation_key="side_brush_time_left",
entity_category=EntityCategory.DIAGNOSTIC,
),
RoborockSensorDescriptionB01(
key="filter_time_left",
value_fn=lambda data: data.filter_time_left,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
translation_key="filter_time_left",
entity_category=EntityCategory.DIAGNOSTIC,
),
RoborockSensorDescriptionB01(
key="sensor_time_left",
value_fn=lambda data: data.sensor_dirty_time_left,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
translation_key="sensor_time_left",
entity_category=EntityCategory.DIAGNOSTIC,
),
RoborockSensorDescriptionB01(
key="mop_life_time_left",
value_fn=lambda data: data.mop_life_time_left,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
translation_key="mop_life_time_left",
entity_category=EntityCategory.DIAGNOSTIC,
),
RoborockSensorDescriptionB01(
key="total_cleaning_time",
value_fn=lambda data: data.real_clean_time,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
translation_key="total_cleaning_time",
entity_category=EntityCategory.DIAGNOSTIC,
),
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -354,6 +430,12 @@ async def async_setup_entry(
for description in A01_SENSOR_DESCRIPTIONS
if description.data_protocol in coordinator.request_protocols
)
entities.extend(
RoborockSensorEntityB01(coordinator, description)
for coordinator in coordinators.b01
for description in Q7_B01_SENSOR_DESCRIPTIONS
if description.value_fn(coordinator.data) is not None
)
async_add_entities(entities)
@@ -440,3 +522,23 @@ class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity):
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
return self.coordinator.data[self.entity_description.data_protocol]
class RoborockSensorEntityB01(RoborockCoordinatedEntityB01, SensorEntity):
"""Representation of a B01 Roborock sensor."""
entity_description: RoborockSensorDescriptionB01
def __init__(
self,
coordinator: RoborockDataUpdateCoordinatorB01,
description: RoborockSensorDescriptionB01,
) -> None:
"""Initialize the entity."""
self.entity_description = description
super().__init__(f"{description.key}_{coordinator.duid_slug}", coordinator)
@property
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -213,6 +213,25 @@
"mop_drying_remaining_time": {
"name": "Mop drying remaining time"
},
"mop_life_time_left": {
"name": "Mop life time left"
},
"q7_status": {
"name": "Status",
"state": {
"charging": "[%key:common::state::charging%]",
"docking": "[%key:component::roborock::entity::sensor::status::state::docking%]",
"mop_airdrying": "Mop air drying",
"mop_cleaning": "Mop cleaning",
"moping": "Mopping",
"paused": "[%key:common::state::paused%]",
"sleeping": "Sleeping",
"sweep_moping": "Sweep mopping",
"sweep_moping_2": "Sweep mopping",
"updating": "[%key:component::roborock::entity::sensor::status::state::updating%]",
"waiting_for_orders": "Waiting for orders"
}
},
"sensor_time_left": {
"name": "Sensor time left"
},

View File

@@ -66,6 +66,7 @@ from .mock_data import (
MAP_DATA,
MULTI_MAP_LIST,
NETWORK_INFO_BY_DEVICE,
Q7_B01_PROPS,
ROBOROCK_RRUID,
ROOM_MAPPING,
SCENES,
@@ -106,6 +107,13 @@ def create_zeo_trait() -> Mock:
return zeo_trait
def create_b01_q7_trait() -> Mock:
"""Create B01 Q7 trait for B01 devices."""
b01_trait = AsyncMock()
b01_trait.query_values.return_value = Q7_B01_PROPS
return b01_trait
@pytest.fixture(name="bypass_api_client_fixture")
def bypass_api_client_fixture() -> None:
"""Skip calls to the API client."""
@@ -332,6 +340,8 @@ def fake_devices_fixture() -> list[FakeDevice]:
fake_device.zeo = create_zeo_trait()
else:
raise ValueError("Unknown A01 category in test HOME_DATA")
elif device_data.pv == "B01":
fake_device.b01_q7_properties = create_b01_q7_trait()
else:
raise ValueError("Unknown pv in test HOME_DATA")
devices.append(fake_device)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from PIL import Image
from roborock.data import (
B01Props,
CleanRecord,
CleanSummary,
Consumable,
@@ -15,6 +16,7 @@ from roborock.data import (
S7Status,
UserData,
ValleyElectricityTimer,
WorkStatusMapping,
)
from vacuum_map_parser_base.config.image_config import ImageConfig
from vacuum_map_parser_base.map_data import ImageData
@@ -530,6 +532,239 @@ HOME_DATA_RAW = {
},
],
},
{
"id": "q7_product_id",
"name": "Roborock Q7 Series",
"model": "roborock.vacuum.sc01",
"category": "robot.vacuum.cleaner",
"capability": 0,
"schema": [
{
"id": 101,
"name": "RPC Request",
"code": "rpc_request",
"mode": "rw",
"type": "RAW",
"property": "null",
},
{
"id": 102,
"name": "RPC Response",
"code": "rpc_response",
"mode": "rw",
"type": "RAW",
"property": "null",
},
{
"id": 120,
"name": "错误代码",
"code": "error_code",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 121,
"name": "设备状态",
"code": "state",
"mode": "ro",
"type": "VALUE",
"property": "null",
},
{
"id": 122,
"name": "设备电量",
"code": "battery",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 123,
"name": "吸力档位",
"code": "fan_power",
"mode": "rw",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 124,
"name": "拖地档位",
"code": "water_box_mode",
"mode": "rw",
"type": "RAW",
"property": "null",
},
{
"id": 125,
"name": "主刷寿命",
"code": "main_brush_life",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 126,
"name": "边刷寿命",
"code": "side_brush_life",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 127,
"name": "滤网寿命",
"code": "filter_life",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 135,
"name": "离线原因",
"code": "offline_status",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 136,
"name": "清洁次数",
"code": "clean_times",
"mode": "rw",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 137,
"name": "扫拖模式",
"code": "cleaning_preference",
"mode": "rw",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 138,
"name": "清洁任务类型",
"code": "clean_task_type",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 139,
"name": "返回基站类型",
"code": "back_type",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 141,
"name": "清洁进度",
"code": "cleaning_progress",
"mode": "ro",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 142,
"name": "窜货信息",
"code": "fc_state",
"mode": "ro",
"type": "RAW",
"property": "null",
},
{
"id": 201,
"name": "启动清洁任务",
"code": "start_clean_task",
"mode": "wo",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 202,
"name": "返回基站任务",
"code": "start_back_dock_task",
"mode": "wo",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 203,
"name": "启动基站任务",
"code": "start_dock_task",
"mode": "wo",
"type": "ENUM",
"property": '{"range": []}',
},
{
"id": 204,
"name": "暂停任务",
"code": "pause",
"mode": "wo",
"type": "RAW",
"property": "null",
},
{
"id": 205,
"name": "继续任务",
"code": "resume",
"mode": "wo",
"type": "RAW",
"property": "null",
},
{
"id": 206,
"name": "结束任务",
"code": "stop",
"mode": "wo",
"type": "RAW",
"property": "null",
},
{
"id": 10000,
"name": "request_cmd",
"code": "request_cmd",
"mode": "wo",
"type": "RAW",
"property": "null",
},
{
"id": 10001,
"name": "response_cmd",
"code": "response_cmd",
"mode": "ro",
"type": "RAW",
"property": "null",
},
{
"id": 10002,
"name": "request_map",
"code": "request_map",
"mode": "ro",
"type": "RAW",
"property": "null",
},
{
"id": 10003,
"name": "response_map",
"code": "response_map",
"mode": "ro",
"type": "RAW",
"property": "null",
},
{
"id": 10004,
"name": "event_report",
"code": "event_report",
"mode": "rw",
"type": "RAW",
"property": "null",
},
],
},
{
"id": "zeo_id",
"name": "Zeo One",
@@ -951,6 +1186,45 @@ HOME_DATA_RAW = {
"silentOtaSwitch": False,
"f": False,
},
{
"duid": "q7_duid",
"name": "Roborock Q7",
"localKey": "q7_local_key",
"productId": "q7_product_id",
"fv": "03.01.71",
"activeTime": 1749513705,
"timeZoneId": "Pacific/Auckland",
"iconUrl": "",
"share": True,
"shareTime": 1754789238,
"online": True,
"pv": "B01",
"tuyaMigrated": False,
"extra": '{"1749518432": "0", "1753581557": "0", "clean_finish": "{}"}',
"sn": "q7_sn",
"deviceStatus": {
"135": 0,
"120": 0,
"121": 8,
"122": 100,
"123": 4,
"124": 2,
"125": 77,
"126": 4294965348,
"127": 54,
"136": 1,
"137": 1,
"138": 0,
"139": 0,
"141": 0,
"142": 0,
},
"silentOtaSwitch": False,
"f": False,
"createTime": 1749513706,
"cid": "DE",
"shareType": "UNLIMITED_TIME",
},
{
"duid": "zeo_duid",
"name": "Zeo One",
@@ -1209,3 +1483,13 @@ SCENES = [
},
),
]
Q7_B01_PROPS = B01Props(
status=WorkStatusMapping.SWEEP_MOPING,
main_brush=5000,
side_brush=3000,
hypa=1500,
main_sensor=500,
mop_life=1200,
real_clean_time=3000,
)

View File

@@ -1172,6 +1172,280 @@
]),
}),
}),
'**REDACTED-4**': dict({
'device': dict({
'activeTime': 1749513705,
'cid': 'DE',
'createTime': 1749513706,
'deviceStatus': dict({
'120': 0,
'121': 8,
'122': 100,
'123': 4,
'124': 2,
'125': 77,
'126': 4294965348,
'127': 54,
'135': 0,
'136': 1,
'137': 1,
'138': 0,
'139': 0,
'141': 0,
'142': 0,
}),
'duid': '**REDACTED**',
'extra': '{"1749518432": "0", "1753581557": "0", "clean_finish": "{}"}',
'f': False,
'fv': '03.01.71',
'iconUrl': '',
'localKey': '**REDACTED**',
'name': 'Roborock Q7',
'online': True,
'productId': 'q7_product_id',
'pv': 'B01',
'share': True,
'shareTime': 1754789238,
'shareType': 'UNLIMITED_TIME',
'silentOtaSwitch': False,
'sn': '**REDACTED**',
'timeZoneId': 'Pacific/Auckland',
'tuyaMigrated': False,
}),
'product': dict({
'capability': 0,
'category': 'robot.vacuum.cleaner',
'id': 'q7_product_id',
'model': 'roborock.vacuum.sc01',
'name': 'Roborock Q7 Series',
'schema': list([
dict({
'code': 'rpc_request',
'id': 101,
'mode': 'rw',
'name': 'RPC Request',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'rpc_response',
'id': 102,
'mode': 'rw',
'name': 'RPC Response',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'error_code',
'id': 120,
'mode': 'ro',
'name': '错误代码',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'state',
'id': 121,
'mode': 'ro',
'name': '设备状态',
'property': 'null',
'type': 'VALUE',
}),
dict({
'code': 'battery',
'id': 122,
'mode': 'ro',
'name': '设备电量',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'fan_power',
'id': 123,
'mode': 'rw',
'name': '吸力档位',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'water_box_mode',
'id': 124,
'mode': 'rw',
'name': '拖地档位',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'main_brush_life',
'id': 125,
'mode': 'ro',
'name': '主刷寿命',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'side_brush_life',
'id': 126,
'mode': 'ro',
'name': '边刷寿命',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'filter_life',
'id': 127,
'mode': 'ro',
'name': '滤网寿命',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'offline_status',
'id': 135,
'mode': 'ro',
'name': '离线原因',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'clean_times',
'id': 136,
'mode': 'rw',
'name': '清洁次数',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'cleaning_preference',
'id': 137,
'mode': 'rw',
'name': '扫拖模式',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'clean_task_type',
'id': 138,
'mode': 'ro',
'name': '清洁任务类型',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'back_type',
'id': 139,
'mode': 'ro',
'name': '返回基站类型',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'cleaning_progress',
'id': 141,
'mode': 'ro',
'name': '清洁进度',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'fc_state',
'id': 142,
'mode': 'ro',
'name': '窜货信息',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'start_clean_task',
'id': 201,
'mode': 'wo',
'name': '启动清洁任务',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'start_back_dock_task',
'id': 202,
'mode': 'wo',
'name': '返回基站任务',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'start_dock_task',
'id': 203,
'mode': 'wo',
'name': '启动基站任务',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
'code': 'pause',
'id': 204,
'mode': 'wo',
'name': '暂停任务',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'resume',
'id': 205,
'mode': 'wo',
'name': '继续任务',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'stop',
'id': 206,
'mode': 'wo',
'name': '结束任务',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'request_cmd',
'id': 10000,
'mode': 'wo',
'name': 'request_cmd',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'response_cmd',
'id': 10001,
'mode': 'ro',
'name': 'response_cmd',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'request_map',
'id': 10002,
'mode': 'ro',
'name': 'request_map',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'response_map',
'id': 10003,
'mode': 'ro',
'name': 'response_map',
'property': 'null',
'type': 'RAW',
}),
dict({
'code': 'event_report',
'id': 10004,
'mode': 'rw',
'name': 'event_report',
'property': 'null',
'type': 'RAW',
}),
]),
}),
}),
}),
})
# ---

View File

@@ -878,5 +878,108 @@
'last_updated': <ANY>,
'state': 'none',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Roborock Q7 Status',
'options': list([
'sleeping',
'waiting_for_orders',
'paused',
'docking',
'charging',
'sweep_moping',
'sweep_moping_2',
'moping',
'updating',
'mop_cleaning',
'mop_airdrying',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'sweep_moping',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Roborock Q7 Main brush time left',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_main_brush_time_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '216.666666666667',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Roborock Q7 Side brush time left',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_side_brush_time_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '150.0',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Roborock Q7 Filter time left',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_filter_time_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '125.0',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Roborock Q7 Sensor time left',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_sensor_time_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '21.6666666666667',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Roborock Q7 Mop life time left',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_mop_life_time_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '160.0',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Roborock Q7 Total cleaning time',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.roborock_q7_total_cleaning_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50.0',
}),
])
# ---

View File

@@ -233,6 +233,7 @@ async def test_stale_device(
"Roborock S7 2 Dock",
"Dyad Pro",
"Zeo One",
"Roborock Q7",
}
fake_devices.pop(0) # Remove one robot
@@ -246,6 +247,7 @@ async def test_stale_device(
"Roborock S7 2 Dock",
"Dyad Pro",
"Zeo One",
"Roborock Q7",
}
@@ -269,6 +271,7 @@ async def test_no_stale_device(
"Roborock S7 2 Dock",
"Dyad Pro",
"Zeo One",
"Roborock Q7",
}
await hass.config_entries.async_reload(mock_roborock_entry.entry_id)
@@ -283,6 +286,7 @@ async def test_no_stale_device(
"Roborock S7 2 Dock",
"Dyad Pro",
"Zeo One",
"Roborock Q7",
}
@@ -440,6 +444,7 @@ async def test_zeo_device_fails_setup(
"Roborock S7 2",
"Roborock S7 2 Dock",
"Dyad Pro",
"Roborock Q7",
# Zeo device is missing
}
@@ -476,4 +481,5 @@ async def test_dyad_device_fails_setup(
"Roborock S7 2 Dock",
# Dyad device is missing
"Zeo One",
"Roborock Q7",
}