Add support for Dyad vacuums to Roborock (#115331)
This commit is contained in:
parent
4defc4a58f
commit
d0f82d6f02
19 changed files with 874 additions and 117 deletions
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@ -12,6 +13,7 @@ from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials
|
|||
from roborock.code_mappings import RoborockCategory
|
||||
from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData
|
||||
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
|
||||
from roborock.version_a01_apis import RoborockMqttClientA01
|
||||
from roborock.web_api import RoborockApiClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -20,13 +22,27 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoborockCoordinators:
|
||||
"""Roborock coordinators type."""
|
||||
|
||||
v1: list[RoborockDataUpdateCoordinator]
|
||||
a01: list[RoborockDataUpdateCoordinatorA01]
|
||||
|
||||
def values(
|
||||
self,
|
||||
) -> list[RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01]:
|
||||
"""Return all coordinators."""
|
||||
return self.v1 + self.a01
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up roborock from a config entry."""
|
||||
|
||||
|
@ -37,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL])
|
||||
_LOGGER.debug("Getting home data")
|
||||
try:
|
||||
home_data = await api_client.get_home_data(user_data)
|
||||
home_data = await api_client.get_home_data_v2(user_data)
|
||||
except RoborockInvalidCredentials as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Invalid credentials",
|
||||
|
@ -66,21 +82,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
return_exceptions=True,
|
||||
)
|
||||
# Valid coordinators are those where we had networking cached or we could get networking
|
||||
valid_coordinators: list[RoborockDataUpdateCoordinator] = [
|
||||
v1_coords = [
|
||||
coord
|
||||
for coord in coordinators
|
||||
if isinstance(coord, RoborockDataUpdateCoordinator)
|
||||
]
|
||||
if len(valid_coordinators) == 0:
|
||||
a01_coords = [
|
||||
coord
|
||||
for coord in coordinators
|
||||
if isinstance(coord, RoborockDataUpdateCoordinatorA01)
|
||||
]
|
||||
if len(v1_coords) + len(a01_coords) == 0:
|
||||
raise ConfigEntryNotReady(
|
||||
"No devices were able to successfully setup",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_coordinators",
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
coordinator.api.device_info.device.duid: coordinator
|
||||
for coordinator in valid_coordinators
|
||||
}
|
||||
valid_coordinators = RoborockCoordinators(v1_coords, a01_coords)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = valid_coordinators
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
@ -92,14 +111,19 @@ def build_setup_functions(
|
|||
user_data: UserData,
|
||||
product_info: dict[str, HomeDataProduct],
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
) -> list[Coroutine[Any, Any, RoborockDataUpdateCoordinator | None]]:
|
||||
) -> list[
|
||||
Coroutine[
|
||||
Any,
|
||||
Any,
|
||||
RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None,
|
||||
]
|
||||
]:
|
||||
"""Create a list of setup functions that can later be called asynchronously."""
|
||||
return [
|
||||
setup_device(
|
||||
hass, user_data, device, product_info[device.product_id], home_data_rooms
|
||||
)
|
||||
for device in device_map.values()
|
||||
if product_info[device.product_id].category == RoborockCategory.VACUUM
|
||||
]
|
||||
|
||||
|
||||
|
@ -109,11 +133,33 @@ async def setup_device(
|
|||
device: HomeDataDevice,
|
||||
product_info: HomeDataProduct,
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None:
|
||||
"""Set up a coordinator for a given device."""
|
||||
if device.pv == "1.0":
|
||||
return await setup_device_v1(
|
||||
hass, user_data, device, product_info, home_data_rooms
|
||||
)
|
||||
if device.pv == "A01":
|
||||
if product_info.category == RoborockCategory.WET_DRY_VAC:
|
||||
return await setup_device_a01(hass, user_data, device, product_info)
|
||||
_LOGGER.info(
|
||||
"Not adding device %s because its protocol version %s or category %s is not supported",
|
||||
device.duid,
|
||||
device.pv,
|
||||
product_info.category.name,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def setup_device_v1(
|
||||
hass: HomeAssistant,
|
||||
user_data: UserData,
|
||||
device: HomeDataDevice,
|
||||
product_info: HomeDataProduct,
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
) -> RoborockDataUpdateCoordinator | None:
|
||||
"""Set up a device Coordinator."""
|
||||
mqtt_client = RoborockMqttClientV1(
|
||||
user_data, DeviceData(device, product_info.model)
|
||||
)
|
||||
mqtt_client = RoborockMqttClientV1(user_data, DeviceData(device, product_info.name))
|
||||
try:
|
||||
networking = await mqtt_client.get_networking()
|
||||
if networking is None:
|
||||
|
@ -170,6 +216,21 @@ async def setup_device(
|
|||
return coordinator
|
||||
|
||||
|
||||
async def setup_device_a01(
|
||||
hass: HomeAssistant,
|
||||
user_data: UserData,
|
||||
device: HomeDataDevice,
|
||||
product_info: HomeDataProduct,
|
||||
) -> RoborockDataUpdateCoordinatorA01 | None:
|
||||
"""Set up a A01 protocol device."""
|
||||
mqtt_client = RoborockMqttClientA01(
|
||||
user_data, DeviceData(device, product_info.name), product_info.category
|
||||
)
|
||||
coord = RoborockDataUpdateCoordinatorA01(hass, device, product_info, mqtt_client)
|
||||
await coord.async_config_entry_first_refresh()
|
||||
return coord
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
|
|
|
@ -18,9 +18,10 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockCoordinatedEntity
|
||||
from .device import RoborockCoordinatedEntityV1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
@ -75,34 +76,33 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Roborock vacuum binary sensors."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
RoborockBinarySensorEntity(
|
||||
f"{description.key}_{slugify(device_id)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for coordinator in coordinators.v1
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.value_fn(coordinator.roborock_device_info.props) is not None
|
||||
)
|
||||
|
||||
|
||||
class RoborockBinarySensorEntity(RoborockCoordinatedEntity, BinarySensorEntity):
|
||||
class RoborockBinarySensorEntity(RoborockCoordinatedEntityV1, BinarySensorEntity):
|
||||
"""Representation of a Roborock binary sensor."""
|
||||
|
||||
entity_description: RoborockBinarySensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
description: RoborockBinarySensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(unique_id, coordinator)
|
||||
super().__init__(
|
||||
f"{description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator,
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
|
|
|
@ -13,9 +13,10 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockEntity
|
||||
from .device import RoborockEntityV1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
@ -68,33 +69,34 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock button platform."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
RoborockButtonEntity(
|
||||
f"{description.key}_{slugify(device_id)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for coordinator in coordinators.v1
|
||||
for description in CONSUMABLE_BUTTON_DESCRIPTIONS
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator)
|
||||
)
|
||||
|
||||
|
||||
class RoborockButtonEntity(RoborockEntity, ButtonEntity):
|
||||
class RoborockButtonEntity(RoborockEntityV1, ButtonEntity):
|
||||
"""A class to define Roborock button entities."""
|
||||
|
||||
entity_description: RoborockButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
entity_description: RoborockButtonDescription,
|
||||
) -> None:
|
||||
"""Create a button entity."""
|
||||
super().__init__(unique_id, coordinator.device_info, coordinator.api)
|
||||
super().__init__(
|
||||
f"{entity_description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator.device_info,
|
||||
coordinator.api,
|
||||
)
|
||||
self.entity_description = entity_description
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
|
|
@ -9,18 +9,21 @@ import logging
|
|||
from roborock import HomeDataRoom
|
||||
from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
|
||||
from roborock.roborock_typing import DeviceProp
|
||||
from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1
|
||||
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
|
||||
from roborock.version_a01_apis import RoborockClientA01
|
||||
|
||||
from homeassistant.const import ATTR_CONNECTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import RoborockHassDeviceInfo, RoborockMapInfo
|
||||
from .models import RoborockA01HassDeviceInfo, RoborockHassDeviceInfo, RoborockMapInfo
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
@ -77,6 +80,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
|||
"Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance",
|
||||
self.roborock_device_info.device.duid,
|
||||
)
|
||||
await self.api.async_disconnect()
|
||||
# We use the cloud api if the local api fails to connect.
|
||||
self.api = self.cloud_api
|
||||
# Right now this should never be called if the cloud api is the primary api,
|
||||
|
@ -137,3 +141,57 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
|||
self.maps[self.current_map].rooms[room.segment_id] = (
|
||||
self._home_data_rooms.get(room.iot_id, "Unknown")
|
||||
)
|
||||
|
||||
@property
|
||||
def duid(self) -> str:
|
||||
"""Get the unique id of the device as specified by Roborock."""
|
||||
return self.roborock_device_info.device.duid
|
||||
|
||||
|
||||
class RoborockDataUpdateCoordinatorA01(
|
||||
DataUpdateCoordinator[
|
||||
dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType]
|
||||
]
|
||||
):
|
||||
"""Class to manage fetching data from the API for A01 devices."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
device: HomeDataDevice,
|
||||
product_info: HomeDataProduct,
|
||||
api: RoborockClientA01,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
|
||||
self.api = api
|
||||
self.device_info = DeviceInfo(
|
||||
name=device.name,
|
||||
identifiers={(DOMAIN, device.duid)},
|
||||
manufacturer="Roborock",
|
||||
model=product_info.model,
|
||||
sw_version=device.fv,
|
||||
)
|
||||
self.request_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] = [
|
||||
RoborockDyadDataProtocol.STATUS,
|
||||
RoborockDyadDataProtocol.POWER,
|
||||
RoborockDyadDataProtocol.MESH_LEFT,
|
||||
RoborockDyadDataProtocol.BRUSH_LEFT,
|
||||
RoborockDyadDataProtocol.ERROR,
|
||||
RoborockDyadDataProtocol.TOTAL_RUN_TIME,
|
||||
]
|
||||
self.roborock_device_info = RoborockA01HassDeviceInfo(device, product_info)
|
||||
|
||||
async def _async_update_data(
|
||||
self,
|
||||
) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType]:
|
||||
return await self.api.update_values(self.request_protocols)
|
||||
|
||||
async def release(self) -> None:
|
||||
"""Disconnect from API."""
|
||||
await self.api.async_release()
|
||||
|
||||
@property
|
||||
def duid(self) -> str:
|
||||
"""Get the unique id of the device as specified by Roborock."""
|
||||
return self.roborock_device_info.device.duid
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from typing import Any
|
||||
|
||||
from roborock.api import RoborockClient
|
||||
from roborock.command_cache import CacheableAttribute
|
||||
from roborock.containers import Consumable, Status
|
||||
from roborock.exceptions import RoborockException
|
||||
|
@ -9,6 +10,7 @@ from roborock.roborock_message import RoborockDataProtocol
|
|||
from roborock.roborock_typing import RoborockCommand
|
||||
from roborock.version_1_apis.roborock_client_v1 import AttributeCache, RoborockClientV1
|
||||
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
|
||||
from roborock.version_a01_apis import RoborockClientA01
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
|
@ -16,7 +18,7 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
|
||||
|
||||
|
||||
class RoborockEntity(Entity):
|
||||
|
@ -28,17 +30,24 @@ class RoborockEntity(Entity):
|
|||
self,
|
||||
unique_id: str,
|
||||
device_info: DeviceInfo,
|
||||
api: RoborockClientV1,
|
||||
api: RoborockClient,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
"""Initialize the Roborock Device."""
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = device_info
|
||||
self._api = api
|
||||
|
||||
@property
|
||||
def api(self) -> RoborockClientV1:
|
||||
"""Returns the api."""
|
||||
return self._api
|
||||
|
||||
class RoborockEntityV1(RoborockEntity):
|
||||
"""Representation of a base Roborock V1 Entity."""
|
||||
|
||||
_api: RoborockClientV1
|
||||
|
||||
def __init__(
|
||||
self, unique_id: str, device_info: DeviceInfo, api: RoborockClientV1
|
||||
) -> None:
|
||||
"""Initialize the Roborock Device."""
|
||||
super().__init__(unique_id, device_info, api)
|
||||
|
||||
def get_cache(self, attribute: CacheableAttribute) -> AttributeCache:
|
||||
"""Get an item from the api cache."""
|
||||
|
@ -66,9 +75,26 @@ class RoborockEntity(Entity):
|
|||
) from err
|
||||
return response
|
||||
|
||||
@property
|
||||
def api(self) -> RoborockClientV1:
|
||||
"""Returns the api."""
|
||||
return self._api
|
||||
|
||||
class RoborockCoordinatedEntity(
|
||||
RoborockEntity, CoordinatorEntity[RoborockDataUpdateCoordinator]
|
||||
|
||||
class RoborockEntityA01(RoborockEntity):
|
||||
"""Representation of a base Roborock Entity for A01 devices."""
|
||||
|
||||
_api: RoborockClientA01
|
||||
|
||||
def __init__(
|
||||
self, unique_id: str, device_info: DeviceInfo, api: RoborockClientA01
|
||||
) -> None:
|
||||
"""Initialize the Roborock Device."""
|
||||
super().__init__(unique_id, device_info, api)
|
||||
|
||||
|
||||
class RoborockCoordinatedEntityV1(
|
||||
RoborockEntityV1, CoordinatorEntity[RoborockDataUpdateCoordinator]
|
||||
):
|
||||
"""Representation of a base a coordinated Roborock Entity."""
|
||||
|
||||
|
@ -83,7 +109,7 @@ class RoborockCoordinatedEntity(
|
|||
| None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
RoborockEntity.__init__(
|
||||
RoborockEntityV1.__init__(
|
||||
self,
|
||||
unique_id=unique_id,
|
||||
device_info=coordinator.device_info,
|
||||
|
@ -138,3 +164,24 @@ class RoborockCoordinatedEntity(
|
|||
self.coordinator.roborock_device_info.props.consumable = value
|
||||
self.coordinator.data = self.coordinator.roborock_device_info.props
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class RoborockCoordinatedEntityA01(
|
||||
RoborockEntityA01, CoordinatorEntity[RoborockDataUpdateCoordinatorA01]
|
||||
):
|
||||
"""Representation of a base a coordinated Roborock Entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinatorA01,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
RoborockEntityA01.__init__(
|
||||
self,
|
||||
unique_id=unique_id,
|
||||
device_info=coordinator.device_info,
|
||||
api=coordinator.api,
|
||||
)
|
||||
CoordinatorEntity.__init__(self, coordinator=coordinator)
|
||||
self._attr_unique_id = unique_id
|
||||
|
|
|
@ -9,8 +9,8 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.const import CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
|
||||
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
|
||||
|
||||
|
@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics(
|
|||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
|
||||
|
|
|
@ -21,9 +21,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from homeassistant.util import slugify
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockCoordinatedEntity
|
||||
from .device import RoborockCoordinatedEntityV1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -33,9 +34,7 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up Roborock image platform."""
|
||||
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
drawables = [
|
||||
drawable
|
||||
for drawable, default_value in DEFAULT_DRAWABLES.items()
|
||||
|
@ -46,7 +45,7 @@ async def async_setup_entry(
|
|||
await asyncio.gather(
|
||||
*(
|
||||
create_coordinator_maps(coord, drawables)
|
||||
for coord in coordinators.values()
|
||||
for coord in coordinators.v1
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -54,7 +53,7 @@ async def async_setup_entry(
|
|||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RoborockMap(RoborockCoordinatedEntity, ImageEntity):
|
||||
class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
"""A class to let you visualize the map."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
@ -70,7 +69,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity):
|
|||
drawables: list[Drawable],
|
||||
) -> None:
|
||||
"""Initialize a Roborock map."""
|
||||
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
|
||||
RoborockCoordinatedEntityV1.__init__(self, unique_id, coordinator)
|
||||
ImageEntity.__init__(self, coordinator.hass)
|
||||
self._attr_name = map_name
|
||||
self.parser = RoborockMapDataParser(
|
||||
|
@ -184,7 +183,7 @@ async def create_coordinator_maps(
|
|||
api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b""
|
||||
entities.append(
|
||||
RoborockMap(
|
||||
f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_info.name}",
|
||||
f"{slugify(coord.duid)}_map_{map_info.name}",
|
||||
coord,
|
||||
map_flag,
|
||||
api_data,
|
||||
|
|
|
@ -26,6 +26,21 @@ class RoborockHassDeviceInfo:
|
|||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoborockA01HassDeviceInfo:
|
||||
"""A model to describe A01 roborock devices."""
|
||||
|
||||
device: HomeDataDevice
|
||||
product: HomeDataProduct
|
||||
|
||||
def as_dict(self) -> dict[str, dict[str, Any]]:
|
||||
"""Turn RoborockA01HassDeviceInfo into a dictionary."""
|
||||
return {
|
||||
"device": self.device.as_dict(),
|
||||
"product": self.product.as_dict(),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoborockMapInfo:
|
||||
"""A model to describe all information about a map we may want."""
|
||||
|
|
|
@ -17,9 +17,10 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockEntity
|
||||
from .device import RoborockEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -54,14 +55,12 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock number platform."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
possible_entities: list[
|
||||
tuple[RoborockDataUpdateCoordinator, RoborockNumberDescription]
|
||||
] = [
|
||||
(coordinator, description)
|
||||
for coordinator in coordinators.values()
|
||||
for coordinator in coordinators.v1
|
||||
for description in NUMBER_DESCRIPTIONS
|
||||
]
|
||||
# We need to check if this function is supported by the device.
|
||||
|
@ -81,7 +80,7 @@ async def async_setup_entry(
|
|||
else:
|
||||
valid_entities.append(
|
||||
RoborockNumberEntity(
|
||||
f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}",
|
||||
f"{description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
|
@ -89,7 +88,7 @@ async def async_setup_entry(
|
|||
async_add_entities(valid_entities)
|
||||
|
||||
|
||||
class RoborockNumberEntity(RoborockEntity, NumberEntity):
|
||||
class RoborockNumberEntity(RoborockEntityV1, NumberEntity):
|
||||
"""A class to let you set options on a Roborock vacuum where the potential options are fixed."""
|
||||
|
||||
entity_description: RoborockNumberDescription
|
||||
|
|
|
@ -14,9 +14,10 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockCoordinatedEntity
|
||||
from .device import RoborockCoordinatedEntityV1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
@ -69,14 +70,10 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up Roborock select platform."""
|
||||
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
RoborockSelectEntity(
|
||||
f"{description.key}_{slugify(device_id)}", coordinator, description, options
|
||||
)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
RoborockSelectEntity(coordinator, description, options)
|
||||
for coordinator in coordinators.v1
|
||||
for description in SELECT_DESCRIPTIONS
|
||||
if (
|
||||
options := description.options_lambda(
|
||||
|
@ -87,21 +84,24 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity):
|
||||
class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||
"""A class to let you set options on a Roborock vacuum where the potential options are fixed."""
|
||||
|
||||
entity_description: RoborockSelectDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
entity_description: RoborockSelectDescription,
|
||||
options: list[str],
|
||||
) -> None:
|
||||
"""Create a select entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(unique_id, coordinator, entity_description.protocol_listener)
|
||||
super().__init__(
|
||||
f"{entity_description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator,
|
||||
entity_description.protocol_listener,
|
||||
)
|
||||
self._attr_options = options
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
|
|
|
@ -6,13 +6,14 @@ from collections.abc import Callable
|
|||
from dataclasses import dataclass
|
||||
import datetime
|
||||
|
||||
from roborock.code_mappings import DyadError, RoborockDyadStateCode
|
||||
from roborock.containers import (
|
||||
RoborockDockErrorCode,
|
||||
RoborockDockTypeCode,
|
||||
RoborockErrorCode,
|
||||
RoborockStateCode,
|
||||
)
|
||||
from roborock.roborock_message import RoborockDataProtocol
|
||||
from roborock.roborock_message import RoborockDataProtocol, RoborockDyadDataProtocol
|
||||
from roborock.roborock_typing import DeviceProp
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
|
@ -32,9 +33,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockCoordinatedEntity
|
||||
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
|
||||
from .device import RoborockCoordinatedEntityA01, RoborockCoordinatedEntityV1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
@ -46,6 +48,13 @@ class RoborockSensorDescription(SensorEntityDescription):
|
|||
protocol_listener: RoborockDataProtocol | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RoborockSensorDescriptionA01(SensorEntityDescription):
|
||||
"""A class that describes Roborock sensors."""
|
||||
|
||||
data_protocol: RoborockDyadDataProtocol
|
||||
|
||||
|
||||
def _dock_error_value_fn(properties: DeviceProp) -> str | None:
|
||||
if (
|
||||
status := properties.status.dock_error_status
|
||||
|
@ -193,41 +202,101 @@ SENSOR_DESCRIPTIONS = [
|
|||
]
|
||||
|
||||
|
||||
A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [
|
||||
RoborockSensorDescriptionA01(
|
||||
key="status",
|
||||
data_protocol=RoborockDyadDataProtocol.STATUS,
|
||||
translation_key="a01_status",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=RoborockDyadStateCode.keys(),
|
||||
),
|
||||
RoborockSensorDescriptionA01(
|
||||
key="battery",
|
||||
data_protocol=RoborockDyadDataProtocol.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
),
|
||||
RoborockSensorDescriptionA01(
|
||||
key="filter_time_left",
|
||||
data_protocol=RoborockDyadDataProtocol.MESH_LEFT,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
translation_key="filter_time_left",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
RoborockSensorDescriptionA01(
|
||||
key="brush_remaining",
|
||||
data_protocol=RoborockDyadDataProtocol.BRUSH_LEFT,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
translation_key="brush_remaining",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
RoborockSensorDescriptionA01(
|
||||
key="error",
|
||||
data_protocol=RoborockDyadDataProtocol.ERROR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="a01_error",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=DyadError.keys(),
|
||||
),
|
||||
RoborockSensorDescriptionA01(
|
||||
key="total_cleaning_time",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
data_protocol=RoborockDyadDataProtocol.TOTAL_RUN_TIME,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
translation_key="total_cleaning_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Roborock vacuum sensors."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
RoborockSensorEntity(
|
||||
f"{description.key}_{slugify(device_id)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
for coordinator in coordinators.v1
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.value_fn(coordinator.roborock_device_info.props) is not None
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockSensorEntityA01(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in coordinators.a01
|
||||
for description in A01_SENSOR_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity):
|
||||
class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity):
|
||||
"""Representation of a Roborock sensor."""
|
||||
|
||||
entity_description: RoborockSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
description: RoborockSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.entity_description = description
|
||||
super().__init__(unique_id, coordinator, description.protocol_listener)
|
||||
super().__init__(
|
||||
f"{description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator,
|
||||
description.protocol_listener,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime.datetime:
|
||||
|
@ -235,3 +304,23 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity):
|
|||
return self.entity_description.value_fn(
|
||||
self.coordinator.roborock_device_info.props
|
||||
)
|
||||
|
||||
|
||||
class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity):
|
||||
"""Representation of a A01 Roborock sensor."""
|
||||
|
||||
entity_description: RoborockSensorDescriptionA01
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RoborockDataUpdateCoordinatorA01,
|
||||
description: RoborockSensorDescriptionA01,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.entity_description = description
|
||||
super().__init__(f"{description.key}_{slugify(coordinator.duid)}", coordinator)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.coordinator.data[self.entity_description.data_protocol]
|
||||
|
|
|
@ -95,6 +95,54 @@
|
|||
}
|
||||
},
|
||||
"sensor": {
|
||||
"a01_error": {
|
||||
"name": "Error",
|
||||
"state": {
|
||||
"none": "[%key:component::roborock::entity::sensor::vacuum_error::state::none%]",
|
||||
"dirty_tank_full": "Dirty tank full",
|
||||
"water_level_sensor_stuck": "Water level sensor stuck.",
|
||||
"clean_tank_empty": "Clean tank empty",
|
||||
"clean_head_entangled": "Cleaning head entangled",
|
||||
"clean_head_too_hot": "Cleaning head too hot.",
|
||||
"fan_protection_e5": "Fan protection",
|
||||
"cleaning_head_blocked": "Cleaning head blocked",
|
||||
"temperature_protection": "Temperature protection",
|
||||
"fan_protection_e4": "[%key:component::roborock::entity::sensor::a01_error::state::fan_protection_e5%]",
|
||||
"fan_protection_e9": "[%key:component::roborock::entity::sensor::a01_error::state::fan_protection_e5%]",
|
||||
"battery_temperature_protection_e0": "[%key:component::roborock::entity::sensor::a01_error::state::temperature_protection%]",
|
||||
"battery_temperature_protection": "Battery temperature protection",
|
||||
"battery_temperature_protection_2": "[%key:component::roborock::entity::sensor::a01_error::state::battery_temperature_protection%]",
|
||||
"power_adapter_error": "Power adapter error",
|
||||
"dirty_charging_contacts": "Clean charging contacts",
|
||||
"low_battery": "[%key:component::roborock::entity::sensor::vacuum_error::state::low_battery%]",
|
||||
"battery_under_10": "Battery under 10%"
|
||||
}
|
||||
},
|
||||
"a01_status": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
"unknown": "[%key:component::roborock::entity::sensor::status::state::unknown%]",
|
||||
"fetching": "Fetching",
|
||||
"fetch_failed": "Fetch failed",
|
||||
"updating": "[%key:component::roborock::entity::sensor::status::state::updating%]",
|
||||
"washing": "Washing",
|
||||
"ready": "Ready",
|
||||
"charging": "[%key:component::roborock::entity::sensor::status::state::charging%]",
|
||||
"mop_washing": "Washing mop",
|
||||
"self_clean_cleaning": "Self clean cleaning",
|
||||
"self_clean_deep_cleaning": "Self clean deep cleaning",
|
||||
"self_clean_rinsing": "Self clean rinsing",
|
||||
"self_clean_dehydrating": "Self clean drying",
|
||||
"drying": "Drying",
|
||||
"ventilating": "Ventilating",
|
||||
"reserving": "Reserving",
|
||||
"mop_washing_paused": "Mop washing paused",
|
||||
"dusting_mode": "Dusting mode"
|
||||
}
|
||||
},
|
||||
"brush_remaining": {
|
||||
"name": "Roller left"
|
||||
},
|
||||
"cleaning_area": {
|
||||
"name": "Cleaning area"
|
||||
},
|
||||
|
|
|
@ -18,9 +18,10 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockEntity
|
||||
from .device import RoborockEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -102,14 +103,12 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock switch platform."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
possible_entities: list[
|
||||
tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription]
|
||||
] = [
|
||||
(coordinator, description)
|
||||
for coordinator in coordinators.values()
|
||||
for coordinator in coordinators.v1
|
||||
for description in SWITCH_DESCRIPTIONS
|
||||
]
|
||||
# We need to check if this function is supported by the device.
|
||||
|
@ -129,7 +128,7 @@ async def async_setup_entry(
|
|||
else:
|
||||
valid_entities.append(
|
||||
RoborockSwitch(
|
||||
f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}",
|
||||
f"{description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
|
@ -137,7 +136,7 @@ async def async_setup_entry(
|
|||
async_add_entities(valid_entities)
|
||||
|
||||
|
||||
class RoborockSwitch(RoborockEntity, SwitchEntity):
|
||||
class RoborockSwitch(RoborockEntityV1, SwitchEntity):
|
||||
"""A class to let you turn functionality on Roborock devices on and off that does need a coordinator."""
|
||||
|
||||
entity_description: RoborockSwitchDescription
|
||||
|
|
|
@ -19,9 +19,10 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockEntity
|
||||
from .device import RoborockEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -118,14 +119,12 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Roborock time platform."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
possible_entities: list[
|
||||
tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription]
|
||||
] = [
|
||||
(coordinator, description)
|
||||
for coordinator in coordinators.values()
|
||||
for coordinator in coordinators.v1
|
||||
for description in TIME_DESCRIPTIONS
|
||||
]
|
||||
# We need to check if this function is supported by the device.
|
||||
|
@ -145,7 +144,7 @@ async def async_setup_entry(
|
|||
else:
|
||||
valid_entities.append(
|
||||
RoborockTimeEntity(
|
||||
f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}",
|
||||
f"{description.key}_{slugify(coordinator.duid)}",
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
|
@ -153,7 +152,7 @@ async def async_setup_entry(
|
|||
async_add_entities(valid_entities)
|
||||
|
||||
|
||||
class RoborockTimeEntity(RoborockEntity, TimeEntity):
|
||||
class RoborockTimeEntity(RoborockEntityV1, TimeEntity):
|
||||
"""A class to let you set options on a Roborock vacuum where the potential options are fixed."""
|
||||
|
||||
entity_description: RoborockTimeDescription
|
||||
|
|
|
@ -23,9 +23,10 @@ from homeassistant.helpers import entity_platform
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import RoborockCoordinators
|
||||
from .const import DOMAIN, GET_MAPS_SERVICE_NAME
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .device import RoborockCoordinatedEntity
|
||||
from .device import RoborockCoordinatedEntityV1
|
||||
|
||||
STATE_CODE_TO_STATE = {
|
||||
RoborockStateCode.starting: STATE_IDLE, # "Starting"
|
||||
|
@ -60,12 +61,11 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Roborock sensor."""
|
||||
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
RoborockVacuum(slugify(device_id), coordinator)
|
||||
for device_id, coordinator in coordinators.items()
|
||||
RoborockVacuum(coordinator)
|
||||
for coordinator in coordinators.v1
|
||||
if isinstance(coordinator, RoborockDataUpdateCoordinator)
|
||||
)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
@ -78,7 +78,7 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
|
||||
class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
"""General Representation of a Roborock vacuum."""
|
||||
|
||||
_attr_icon = "mdi:robot-vacuum"
|
||||
|
@ -99,14 +99,13 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize a vacuum."""
|
||||
StateVacuumEntity.__init__(self)
|
||||
RoborockCoordinatedEntity.__init__(
|
||||
RoborockCoordinatedEntityV1.__init__(
|
||||
self,
|
||||
unique_id,
|
||||
slugify(coordinator.duid),
|
||||
coordinator,
|
||||
listener_request=[
|
||||
RoborockDataProtocol.FAN_POWER,
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
"""Global fixtures for Roborock integration."""
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from roborock import RoomMapping
|
||||
from roborock.code_mappings import DyadError, RoborockDyadStateCode
|
||||
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
|
||||
from roborock.version_a01_apis import RoborockMqttClientA01
|
||||
|
||||
from homeassistant.components.roborock.const import (
|
||||
CONF_BASE_URL,
|
||||
|
@ -28,6 +32,28 @@ from .mock_data import (
|
|||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
class A01Mock(RoborockMqttClientA01):
|
||||
"""A class to mock the A01 client."""
|
||||
|
||||
def __init__(self, user_data, device_info, category) -> None:
|
||||
"""Initialize the A01Mock."""
|
||||
super().__init__(user_data, device_info, category)
|
||||
self.protocol_responses = {
|
||||
RoborockDyadDataProtocol.STATUS: RoborockDyadStateCode.drying.name,
|
||||
RoborockDyadDataProtocol.POWER: 100,
|
||||
RoborockDyadDataProtocol.MESH_LEFT: 111,
|
||||
RoborockDyadDataProtocol.BRUSH_LEFT: 222,
|
||||
RoborockDyadDataProtocol.ERROR: DyadError.none.name,
|
||||
RoborockDyadDataProtocol.TOTAL_RUN_TIME: 213,
|
||||
}
|
||||
|
||||
async def update_values(
|
||||
self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
|
||||
):
|
||||
"""Update values with a predetermined response that can be overridden."""
|
||||
return {prot: self.protocol_responses[prot] for prot in dyad_data_protocols}
|
||||
|
||||
|
||||
@pytest.fixture(name="bypass_api_fixture")
|
||||
def bypass_api_fixture() -> None:
|
||||
"""Skip calls to the API."""
|
||||
|
@ -35,7 +61,7 @@ def bypass_api_fixture() -> None:
|
|||
patch("homeassistant.components.roborock.RoborockMqttClientV1.async_connect"),
|
||||
patch("homeassistant.components.roborock.RoborockMqttClientV1._send_command"),
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
return_value=HOME_DATA,
|
||||
),
|
||||
patch(
|
||||
|
@ -95,6 +121,23 @@ def bypass_api_fixture() -> None:
|
|||
"homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1",
|
||||
return_value=b"123",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockClientA01",
|
||||
A01Mock,
|
||||
),
|
||||
patch("homeassistant.components.roborock.RoborockMqttClientA01", A01Mock),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bypass_api_fixture_v1_only(bypass_api_fixture) -> None:
|
||||
"""Bypass api for tests that require only having v1 devices."""
|
||||
home_data_copy = deepcopy(HOME_DATA)
|
||||
home_data_copy.received_devices = []
|
||||
with patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
return_value=home_data_copy,
|
||||
):
|
||||
yield
|
||||
|
||||
|
|
|
@ -588,6 +588,369 @@
|
|||
}),
|
||||
}),
|
||||
}),
|
||||
'**REDACTED-2**': dict({
|
||||
'api': dict({
|
||||
'misc_info': dict({
|
||||
}),
|
||||
}),
|
||||
'roborock_device_info': dict({
|
||||
'device': dict({
|
||||
'activeTime': 1700754026,
|
||||
'deviceStatus': dict({
|
||||
'10001': '{"f":"t"}',
|
||||
'10002': '',
|
||||
'10004': '{"sid_in_use":25,"sid_version":5,"location":"de","bom":"A.03.0291","language":"en"}',
|
||||
'10005': '{"sn":"dyad_sn","ssid":"dyad_ssid","timezone":"Europe/Stockholm","posix_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ip":"1.123.12.1","mac":"b0:4a:33:33:33:33","oba":{"language":"en","name":"A.03.0291_CE","bom":"A.03.0291","location":"de","wifiplan":"EU","timezone":"CET-1CEST,M3.5.0,M10.5.0/3;Europe/Berlin","logserver":"awsde0","featureset":"0"}"}',
|
||||
'10007': '{"mqttOtaData":{"mqttOtaStatus":{"status":"IDLE"}}}',
|
||||
'200': 0,
|
||||
'201': 3,
|
||||
'202': 0,
|
||||
'203': 2,
|
||||
'204': 1,
|
||||
'205': 1,
|
||||
'206': 3,
|
||||
'207': 4,
|
||||
'208': 1,
|
||||
'209': 100,
|
||||
'210': 0,
|
||||
'212': 1,
|
||||
'213': 1,
|
||||
'214': 513,
|
||||
'215': 513,
|
||||
'216': 0,
|
||||
'221': 100,
|
||||
'222': 0,
|
||||
'223': 2,
|
||||
'224': 1,
|
||||
'225': 360,
|
||||
'226': 0,
|
||||
'227': 1320,
|
||||
'228': 360,
|
||||
'229': '000,000,003,000,005,000,000,000,003,000,005,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,012,003,000,000',
|
||||
'230': 352,
|
||||
'235': 0,
|
||||
'237': 0,
|
||||
}),
|
||||
'duid': '**REDACTED**',
|
||||
'f': False,
|
||||
'fv': '01.12.34',
|
||||
'iconUrl': '',
|
||||
'localKey': '**REDACTED**',
|
||||
'name': 'Dyad Pro',
|
||||
'online': True,
|
||||
'productId': 'dyad_product',
|
||||
'pv': 'A01',
|
||||
'share': True,
|
||||
'shareTime': 1701367095,
|
||||
'silentOtaSwitch': False,
|
||||
'timeZoneId': 'Europe/Stockholm',
|
||||
'tuyaMigrated': False,
|
||||
}),
|
||||
'product': dict({
|
||||
'capability': 2,
|
||||
'category': 'roborock.wetdryvac',
|
||||
'id': 'dyad_product',
|
||||
'model': 'roborock.wetdryvac.a56',
|
||||
'name': 'Roborock Dyad Pro',
|
||||
'schema': list([
|
||||
dict({
|
||||
'code': 'drying_status',
|
||||
'id': '134',
|
||||
'mode': 'ro',
|
||||
'name': '烘干状态',
|
||||
'type': 'RAW',
|
||||
}),
|
||||
dict({
|
||||
'code': 'start',
|
||||
'id': '200',
|
||||
'mode': 'rw',
|
||||
'name': '启停',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'status',
|
||||
'id': '201',
|
||||
'mode': 'ro',
|
||||
'name': '状态',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'self_clean_mode',
|
||||
'id': '202',
|
||||
'mode': 'rw',
|
||||
'name': '自清洁模式',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'self_clean_level',
|
||||
'id': '203',
|
||||
'mode': 'rw',
|
||||
'name': '自清洁强度',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'warm_level',
|
||||
'id': '204',
|
||||
'mode': 'rw',
|
||||
'name': '烘干强度',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'clean_mode',
|
||||
'id': '205',
|
||||
'mode': 'rw',
|
||||
'name': '洗地模式',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'suction',
|
||||
'id': '206',
|
||||
'mode': 'rw',
|
||||
'name': '吸力',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'water_level',
|
||||
'id': '207',
|
||||
'mode': 'rw',
|
||||
'name': '水量',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'brush_speed',
|
||||
'id': '208',
|
||||
'mode': 'rw',
|
||||
'name': '滚刷转速',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'power',
|
||||
'id': '209',
|
||||
'mode': 'ro',
|
||||
'name': '电量',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'countdown_time',
|
||||
'id': '210',
|
||||
'mode': 'rw',
|
||||
'name': '预约时间',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'auto_self_clean_set',
|
||||
'id': '212',
|
||||
'mode': 'rw',
|
||||
'name': '自动自清洁',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'auto_dry',
|
||||
'id': '213',
|
||||
'mode': 'rw',
|
||||
'name': '自动烘干',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'mesh_left',
|
||||
'id': '214',
|
||||
'mode': 'ro',
|
||||
'name': '滤网已工作时间',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'brush_left',
|
||||
'id': '215',
|
||||
'mode': 'ro',
|
||||
'name': '滚刷已工作时间',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'error',
|
||||
'id': '216',
|
||||
'mode': 'ro',
|
||||
'name': '错误值',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'mesh_reset',
|
||||
'id': '218',
|
||||
'mode': 'rw',
|
||||
'name': '滤网重置',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'brush_reset',
|
||||
'id': '219',
|
||||
'mode': 'rw',
|
||||
'name': '滚刷重置',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'volume_set',
|
||||
'id': '221',
|
||||
'mode': 'rw',
|
||||
'name': '音量',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'stand_lock_auto_run',
|
||||
'id': '222',
|
||||
'mode': 'rw',
|
||||
'name': '直立解锁自动运行开关',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'auto_self_clean_set_mode',
|
||||
'id': '223',
|
||||
'mode': 'rw',
|
||||
'name': '自动自清洁 - 模式',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'auto_dry_mode',
|
||||
'id': '224',
|
||||
'mode': 'rw',
|
||||
'name': '自动烘干 - 模式',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'silent_dry_duration',
|
||||
'id': '225',
|
||||
'mode': 'rw',
|
||||
'name': '静音烘干时长',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'silent_mode',
|
||||
'id': '226',
|
||||
'mode': 'rw',
|
||||
'name': '勿扰模式开关',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'silent_mode_start_time',
|
||||
'id': '227',
|
||||
'mode': 'rw',
|
||||
'name': '勿扰开启时间',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'silent_mode_end_time',
|
||||
'id': '228',
|
||||
'mode': 'rw',
|
||||
'name': '勿扰结束时间',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'recent_run_time',
|
||||
'id': '229',
|
||||
'mode': 'rw',
|
||||
'name': '近30天每天洗地时长',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'total_run_time',
|
||||
'id': '230',
|
||||
'mode': 'rw',
|
||||
'name': '洗地总时长',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'feature_info',
|
||||
'id': '235',
|
||||
'mode': 'ro',
|
||||
'name': 'featureinfo',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'recover_settings',
|
||||
'id': '236',
|
||||
'mode': 'rw',
|
||||
'name': '恢复初始设置',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'dry_countdown',
|
||||
'id': '237',
|
||||
'mode': 'ro',
|
||||
'name': '烘干倒计时',
|
||||
'type': 'VALUE',
|
||||
}),
|
||||
dict({
|
||||
'code': 'id_query',
|
||||
'id': '10000',
|
||||
'mode': 'rw',
|
||||
'name': 'ID点数据查询',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'f_c',
|
||||
'id': '10001',
|
||||
'mode': 'ro',
|
||||
'name': '防串货',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'schedule_task',
|
||||
'id': '10002',
|
||||
'mode': 'rw',
|
||||
'name': '定时任务',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'snd_switch',
|
||||
'id': '10003',
|
||||
'mode': 'rw',
|
||||
'name': '语音包切换',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'snd_state',
|
||||
'id': '10004',
|
||||
'mode': 'rw',
|
||||
'name': '语音包/OBA信息',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'product_info',
|
||||
'id': '10005',
|
||||
'mode': 'ro',
|
||||
'name': '产品信息',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'privacy_info',
|
||||
'id': '10006',
|
||||
'mode': 'rw',
|
||||
'name': '隐私协议',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'ota_nfo',
|
||||
'id': '10007',
|
||||
'mode': 'ro',
|
||||
'name': 'OTA info',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'rpc_req',
|
||||
'id': '10101',
|
||||
'mode': 'wo',
|
||||
'name': 'rpc req',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
dict({
|
||||
'code': 'rpc_resp',
|
||||
'id': '10102',
|
||||
'mode': 'ro',
|
||||
'name': 'rpc resp',
|
||||
'type': 'STRING',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Test for Roborock init."""
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from roborock import RoborockException, RoborockInvalidCredentials
|
||||
|
||||
from homeassistant.components.roborock.const import DOMAIN
|
||||
|
@ -9,6 +11,8 @@ from homeassistant.config_entries import ConfigEntryState
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .mock_data import HOME_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
|
@ -34,7 +38,7 @@ async def test_config_entry_not_ready(
|
|||
"""Test that when coordinator update fails, entry retries."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop",
|
||||
|
@ -51,7 +55,7 @@ async def test_config_entry_not_ready_home_data(
|
|||
"""Test that when we fail to get home data, entry retries."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
side_effect=RoborockException(),
|
||||
),
|
||||
patch(
|
||||
|
@ -64,7 +68,9 @@ async def test_config_entry_not_ready_home_data(
|
|||
|
||||
|
||||
async def test_get_networking_fails(
|
||||
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture
|
||||
hass: HomeAssistant,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
bypass_api_fixture_v1_only,
|
||||
) -> None:
|
||||
"""Test that when networking fails, we attempt to retry."""
|
||||
with patch(
|
||||
|
@ -76,7 +82,9 @@ async def test_get_networking_fails(
|
|||
|
||||
|
||||
async def test_get_networking_fails_none(
|
||||
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture
|
||||
hass: HomeAssistant,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
bypass_api_fixture_v1_only,
|
||||
) -> None:
|
||||
"""Test that when networking returns None, we attempt to retry."""
|
||||
with patch(
|
||||
|
@ -88,7 +96,9 @@ async def test_get_networking_fails_none(
|
|||
|
||||
|
||||
async def test_cloud_client_fails_props(
|
||||
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture
|
||||
hass: HomeAssistant,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
bypass_api_fixture_v1_only,
|
||||
) -> None:
|
||||
"""Test that if networking succeeds, but we can't communicate with the vacuum, we can't get props, fail."""
|
||||
with (
|
||||
|
@ -106,7 +116,9 @@ async def test_cloud_client_fails_props(
|
|||
|
||||
|
||||
async def test_local_client_fails_props(
|
||||
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture
|
||||
hass: HomeAssistant,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
bypass_api_fixture_v1_only,
|
||||
) -> None:
|
||||
"""Test that if networking succeeds, but we can't communicate locally with the vacuum, we can't get props, fail."""
|
||||
with patch(
|
||||
|
@ -118,7 +130,9 @@ async def test_local_client_fails_props(
|
|||
|
||||
|
||||
async def test_fails_maps_continue(
|
||||
hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture
|
||||
hass: HomeAssistant,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
bypass_api_fixture_v1_only,
|
||||
) -> None:
|
||||
"""Test that if we fail to get the maps, we still setup."""
|
||||
with patch(
|
||||
|
@ -136,7 +150,7 @@ async def test_reauth_started(
|
|||
) -> None:
|
||||
"""Test reauth flow started."""
|
||||
with patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
side_effect=RoborockInvalidCredentials(),
|
||||
):
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
|
@ -145,3 +159,21 @@ async def test_reauth_started(
|
|||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["step_id"] == "reauth_confirm"
|
||||
|
||||
|
||||
async def test_not_supported_protocol(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that we output a message on incorrect protocol."""
|
||||
home_data_copy = deepcopy(HOME_DATA)
|
||||
home_data_copy.received_devices[0].pv = "random"
|
||||
with patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
return_value=home_data_copy,
|
||||
):
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
assert "because its protocol version random" in caplog.text
|
||||
|
|
|
@ -21,7 +21,7 @@ from tests.common import MockConfigEntry
|
|||
|
||||
async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None:
|
||||
"""Test sensors and check test values are correctly set."""
|
||||
assert len(hass.states.async_all("sensor")) == 28
|
||||
assert len(hass.states.async_all("sensor")) == 34
|
||||
assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str(
|
||||
MAIN_BRUSH_REPLACE_TIME - 74382
|
||||
)
|
||||
|
@ -54,6 +54,12 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non
|
|||
hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state
|
||||
== "2023-01-01T03:43:58+00:00"
|
||||
)
|
||||
assert hass.states.get("sensor.dyad_pro_status").state == "drying"
|
||||
assert hass.states.get("sensor.dyad_pro_battery").state == "100"
|
||||
assert hass.states.get("sensor.dyad_pro_filter_time_left").state == "111"
|
||||
assert hass.states.get("sensor.dyad_pro_roller_left").state == "222"
|
||||
assert hass.states.get("sensor.dyad_pro_error").state == "none"
|
||||
assert hass.states.get("sensor.dyad_pro_total_cleaning_time").state == "213"
|
||||
|
||||
|
||||
async def test_listener_update(
|
||||
|
|
Loading…
Add table
Reference in a new issue