diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index a63fa0e65c1..3c419873b66 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -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() diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index baf1973cc00..fe070e10321 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -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 diff --git a/homeassistant/components/roborock/entity.py b/homeassistant/components/roborock/entity.py index 07b4d7ae91e..2dea15e1e96 100644 --- a/homeassistant/components/roborock/entity.py +++ b/homeassistant/components/roborock/entity.py @@ -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 diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 6eb633ca939..24f2d340e38 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -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) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index e51deb7f79d..14013131f27 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -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" }, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index aaf9a69e112..0378ad98cba 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -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) diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index c9cd219e35b..80a51dff45d 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -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, +) diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 55e8af1f859..6cc9db53a6f 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -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', + }), + ]), + }), + }), }), }) # --- diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 61f7a1066d7..bdf797a079e 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -878,5 +878,108 @@ 'last_updated': , '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': , + 'entity_id': 'sensor.roborock_q7_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'sweep_moping', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q7 Main brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q7_main_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '216.666666666667', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q7 Side brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q7_side_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '150.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q7 Filter time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q7_filter_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '125.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q7 Sensor time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q7_sensor_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.6666666666667', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q7 Mop life time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q7_mop_life_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '160.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock Q7 Total cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_q7_total_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.0', + }), ]) # --- diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 8ed1ebaad16..034d8b3c1f9 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -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", }