mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 06:05:26 +01:00
Add basic support for Q7 devices (#159274)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user