mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Add listeners for roborock (#103651)
* Add listeners for roborock * add tests * decrease test complexity
This commit is contained in:
parent
f8e3f1497c
commit
6ef194f992
@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from roborock.api import AttributeCache, RoborockClient
|
from roborock.api import AttributeCache, RoborockClient
|
||||||
from roborock.cloud_api import RoborockMqttClient
|
from roborock.cloud_api import RoborockMqttClient
|
||||||
from roborock.command_cache import CacheableAttribute
|
from roborock.command_cache import CacheableAttribute
|
||||||
from roborock.containers import Status
|
from roborock.containers import Consumable, Status
|
||||||
from roborock.exceptions import RoborockException
|
from roborock.exceptions import RoborockException
|
||||||
from roborock.roborock_typing import RoborockCommand
|
from roborock.roborock_typing import RoborockCommand
|
||||||
|
|
||||||
@ -97,3 +97,12 @@ class RoborockCoordinatedEntity(
|
|||||||
res = await super().send(command, params)
|
res = await super().send(command, params)
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_refresh()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def _update_from_listener(self, value: Status | Consumable):
|
||||||
|
"""Update the status or consumable data from a listener and then write the new entity state."""
|
||||||
|
if isinstance(value, Status):
|
||||||
|
self.coordinator.roborock_device_info.props.status = value
|
||||||
|
else:
|
||||||
|
self.coordinator.roborock_device_info.props.consumable = value
|
||||||
|
self.coordinator.data = self.coordinator.roborock_device_info.props
|
||||||
|
self.async_write_ha_state()
|
||||||
|
@ -3,6 +3,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from roborock.containers import Status
|
from roborock.containers import Status
|
||||||
|
from roborock.roborock_message import RoborockDataProtocol
|
||||||
from roborock.roborock_typing import RoborockCommand
|
from roborock.roborock_typing import RoborockCommand
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
@ -37,6 +38,8 @@ class RoborockSelectDescription(
|
|||||||
):
|
):
|
||||||
"""Class to describe an Roborock select entity."""
|
"""Class to describe an Roborock select entity."""
|
||||||
|
|
||||||
|
protocol_listener: RoborockDataProtocol | None = None
|
||||||
|
|
||||||
|
|
||||||
SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
|
SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
|
||||||
RoborockSelectDescription(
|
RoborockSelectDescription(
|
||||||
@ -49,6 +52,7 @@ SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
|
|||||||
if data.water_box_mode is not None
|
if data.water_box_mode is not None
|
||||||
else None,
|
else None,
|
||||||
parameter_lambda=lambda key, status: [status.get_mop_intensity_code(key)],
|
parameter_lambda=lambda key, status: [status.get_mop_intensity_code(key)],
|
||||||
|
protocol_listener=RoborockDataProtocol.WATER_BOX_MODE,
|
||||||
),
|
),
|
||||||
RoborockSelectDescription(
|
RoborockSelectDescription(
|
||||||
key="mop_mode",
|
key="mop_mode",
|
||||||
@ -105,6 +109,8 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity):
|
|||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
super().__init__(unique_id, coordinator)
|
super().__init__(unique_id, coordinator)
|
||||||
self._attr_options = options
|
self._attr_options = options
|
||||||
|
if (protocol := self.entity_description.protocol_listener) is not None:
|
||||||
|
self.api.add_listener(protocol, self._update_from_listener, self.api.cache)
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Set the option."""
|
"""Set the option."""
|
||||||
|
@ -11,6 +11,7 @@ from roborock.containers import (
|
|||||||
RoborockErrorCode,
|
RoborockErrorCode,
|
||||||
RoborockStateCode,
|
RoborockStateCode,
|
||||||
)
|
)
|
||||||
|
from roborock.roborock_message import RoborockDataProtocol
|
||||||
from roborock.roborock_typing import DeviceProp
|
from roborock.roborock_typing import DeviceProp
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@ -48,6 +49,8 @@ class RoborockSensorDescription(
|
|||||||
):
|
):
|
||||||
"""A class that describes Roborock sensors."""
|
"""A class that describes Roborock sensors."""
|
||||||
|
|
||||||
|
protocol_listener: RoborockDataProtocol | None = None
|
||||||
|
|
||||||
|
|
||||||
def _dock_error_value_fn(properties: DeviceProp) -> str | None:
|
def _dock_error_value_fn(properties: DeviceProp) -> str | None:
|
||||||
if (
|
if (
|
||||||
@ -67,6 +70,7 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
translation_key="main_brush_time_left",
|
translation_key="main_brush_time_left",
|
||||||
value_fn=lambda data: data.consumable.main_brush_time_left,
|
value_fn=lambda data: data.consumable.main_brush_time_left,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
protocol_listener=RoborockDataProtocol.MAIN_BRUSH_WORK_TIME,
|
||||||
),
|
),
|
||||||
RoborockSensorDescription(
|
RoborockSensorDescription(
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
@ -76,6 +80,7 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
translation_key="side_brush_time_left",
|
translation_key="side_brush_time_left",
|
||||||
value_fn=lambda data: data.consumable.side_brush_time_left,
|
value_fn=lambda data: data.consumable.side_brush_time_left,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
protocol_listener=RoborockDataProtocol.SIDE_BRUSH_WORK_TIME,
|
||||||
),
|
),
|
||||||
RoborockSensorDescription(
|
RoborockSensorDescription(
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
@ -85,6 +90,7 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
translation_key="filter_time_left",
|
translation_key="filter_time_left",
|
||||||
value_fn=lambda data: data.consumable.filter_time_left,
|
value_fn=lambda data: data.consumable.filter_time_left,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
protocol_listener=RoborockDataProtocol.FILTER_WORK_TIME,
|
||||||
),
|
),
|
||||||
RoborockSensorDescription(
|
RoborockSensorDescription(
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
@ -120,6 +126,7 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
value_fn=lambda data: data.status.state_name,
|
value_fn=lambda data: data.status.state_name,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
options=RoborockStateCode.keys(),
|
options=RoborockStateCode.keys(),
|
||||||
|
protocol_listener=RoborockDataProtocol.STATE,
|
||||||
),
|
),
|
||||||
RoborockSensorDescription(
|
RoborockSensorDescription(
|
||||||
key="cleaning_area",
|
key="cleaning_area",
|
||||||
@ -145,6 +152,7 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
value_fn=lambda data: data.status.error_code_name,
|
value_fn=lambda data: data.status.error_code_name,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
options=RoborockErrorCode.keys(),
|
options=RoborockErrorCode.keys(),
|
||||||
|
protocol_listener=RoborockDataProtocol.ERROR_CODE,
|
||||||
),
|
),
|
||||||
RoborockSensorDescription(
|
RoborockSensorDescription(
|
||||||
key="battery",
|
key="battery",
|
||||||
@ -152,6 +160,7 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
protocol_listener=RoborockDataProtocol.BATTERY,
|
||||||
),
|
),
|
||||||
RoborockSensorDescription(
|
RoborockSensorDescription(
|
||||||
key="last_clean_start",
|
key="last_clean_start",
|
||||||
@ -238,6 +247,8 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity):
|
|||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(unique_id, coordinator)
|
super().__init__(unique_id, coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
if (protocol := self.entity_description.protocol_listener) is not None:
|
||||||
|
self.api.add_listener(protocol, self._update_from_listener, self.api.cache)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType | datetime.datetime:
|
def native_value(self) -> StateType | datetime.datetime:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from roborock.code_mappings import RoborockStateCode
|
from roborock.code_mappings import RoborockStateCode
|
||||||
|
from roborock.roborock_message import RoborockDataProtocol
|
||||||
from roborock.roborock_typing import RoborockCommand
|
from roborock.roborock_typing import RoborockCommand
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
@ -94,6 +95,12 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
|
|||||||
StateVacuumEntity.__init__(self)
|
StateVacuumEntity.__init__(self)
|
||||||
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
|
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
|
||||||
self._attr_fan_speed_list = self._device_status.fan_power_options
|
self._attr_fan_speed_list = self._device_status.fan_power_options
|
||||||
|
self.api.add_listener(
|
||||||
|
RoborockDataProtocol.FAN_POWER, self._update_from_listener, self.api.cache
|
||||||
|
)
|
||||||
|
self.api.add_listener(
|
||||||
|
RoborockDataProtocol.STATE, self._update_from_listener, self.api.cache
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def state(self) -> str | None:
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
"""Test Roborock Sensors."""
|
"""Test Roborock Sensors."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from roborock import DeviceData, HomeDataDevice
|
||||||
|
from roborock.cloud_api import RoborockMqttClient
|
||||||
from roborock.const import (
|
from roborock.const import (
|
||||||
FILTER_REPLACE_TIME,
|
FILTER_REPLACE_TIME,
|
||||||
MAIN_BRUSH_REPLACE_TIME,
|
MAIN_BRUSH_REPLACE_TIME,
|
||||||
SENSOR_DIRTY_REPLACE_TIME,
|
SENSOR_DIRTY_REPLACE_TIME,
|
||||||
SIDE_BRUSH_REPLACE_TIME,
|
SIDE_BRUSH_REPLACE_TIME,
|
||||||
)
|
)
|
||||||
|
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .mock_data import CONSUMABLE, STATUS, USER_DATA
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
@ -47,3 +53,41 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non
|
|||||||
hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state
|
hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state
|
||||||
== "2023-01-01T03:43:58+00:00"
|
== "2023-01-01T03:43:58+00:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_listener_update(
|
||||||
|
hass: HomeAssistant, setup_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test that when we receive a mqtt topic, we successfully update the entity."""
|
||||||
|
assert hass.states.get("sensor.roborock_s7_maxv_status").state == "charging"
|
||||||
|
# Listeners are global based on uuid - so this is okay
|
||||||
|
client = RoborockMqttClient(
|
||||||
|
USER_DATA, DeviceData(device=HomeDataDevice("abc123", "", "", "", ""), model="")
|
||||||
|
)
|
||||||
|
# Test Status
|
||||||
|
with patch("roborock.api.AttributeCache.value", STATUS.as_dict()):
|
||||||
|
# Symbolizes a mqtt message coming in
|
||||||
|
client.on_message_received(
|
||||||
|
[
|
||||||
|
RoborockMessage(
|
||||||
|
protocol=RoborockMessageProtocol.GENERAL_REQUEST,
|
||||||
|
payload=b'{"t": 1699464794, "dps": {"121": 5}}',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# Test consumable
|
||||||
|
assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str(
|
||||||
|
FILTER_REPLACE_TIME - 74382
|
||||||
|
)
|
||||||
|
with patch("roborock.api.AttributeCache.value", CONSUMABLE.as_dict()):
|
||||||
|
client.on_message_received(
|
||||||
|
[
|
||||||
|
RoborockMessage(
|
||||||
|
protocol=RoborockMessageProtocol.GENERAL_REQUEST,
|
||||||
|
payload=b'{"t": 1699464794, "dps": {"127": 743}}',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str(
|
||||||
|
FILTER_REPLACE_TIME - 743
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user