Skip to content

Commit

Permalink
Add listeners for roborock (#103651)
Browse files Browse the repository at this point in the history
* Add listeners for roborock

* add tests

* decrease test complexity
  • Loading branch information
Lash-L committed Nov 20, 2023
1 parent f8e3f14 commit 6ef194f
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 1 deletion.
11 changes: 10 additions & 1 deletion homeassistant/components/roborock/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from roborock.api import AttributeCache, RoborockClient
from roborock.cloud_api import RoborockMqttClient
from roborock.command_cache import CacheableAttribute
from roborock.containers import Status
from roborock.containers import Consumable, Status
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand

Expand Down Expand Up @@ -97,3 +97,12 @@ async def send(
res = await super().send(command, params)
await self.coordinator.async_refresh()
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()
6 changes: 6 additions & 0 deletions homeassistant/components/roborock/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import dataclass

from roborock.containers import Status
from roborock.roborock_message import RoborockDataProtocol
from roborock.roborock_typing import RoborockCommand

from homeassistant.components.select import SelectEntity, SelectEntityDescription
Expand Down Expand Up @@ -37,6 +38,8 @@ class RoborockSelectDescription(
):
"""Class to describe an Roborock select entity."""

protocol_listener: RoborockDataProtocol | None = None


SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
RoborockSelectDescription(
Expand All @@ -49,6 +52,7 @@ class RoborockSelectDescription(
if data.water_box_mode is not None
else None,
parameter_lambda=lambda key, status: [status.get_mop_intensity_code(key)],
protocol_listener=RoborockDataProtocol.WATER_BOX_MODE,
),
RoborockSelectDescription(
key="mop_mode",
Expand Down Expand Up @@ -105,6 +109,8 @@ def __init__(
self.entity_description = entity_description
super().__init__(unique_id, coordinator)
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:
"""Set the option."""
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/roborock/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
RoborockErrorCode,
RoborockStateCode,
)
from roborock.roborock_message import RoborockDataProtocol
from roborock.roborock_typing import DeviceProp

from homeassistant.components.sensor import (
Expand Down Expand Up @@ -48,6 +49,8 @@ class RoborockSensorDescription(
):
"""A class that describes Roborock sensors."""

protocol_listener: RoborockDataProtocol | None = None


def _dock_error_value_fn(properties: DeviceProp) -> str | None:
if (
Expand All @@ -67,6 +70,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None:
translation_key="main_brush_time_left",
value_fn=lambda data: data.consumable.main_brush_time_left,
entity_category=EntityCategory.DIAGNOSTIC,
protocol_listener=RoborockDataProtocol.MAIN_BRUSH_WORK_TIME,
),
RoborockSensorDescription(
native_unit_of_measurement=UnitOfTime.SECONDS,
Expand All @@ -76,6 +80,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None:
translation_key="side_brush_time_left",
value_fn=lambda data: data.consumable.side_brush_time_left,
entity_category=EntityCategory.DIAGNOSTIC,
protocol_listener=RoborockDataProtocol.SIDE_BRUSH_WORK_TIME,
),
RoborockSensorDescription(
native_unit_of_measurement=UnitOfTime.SECONDS,
Expand All @@ -85,6 +90,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None:
translation_key="filter_time_left",
value_fn=lambda data: data.consumable.filter_time_left,
entity_category=EntityCategory.DIAGNOSTIC,
protocol_listener=RoborockDataProtocol.FILTER_WORK_TIME,
),
RoborockSensorDescription(
native_unit_of_measurement=UnitOfTime.SECONDS,
Expand Down Expand Up @@ -120,6 +126,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None:
value_fn=lambda data: data.status.state_name,
entity_category=EntityCategory.DIAGNOSTIC,
options=RoborockStateCode.keys(),
protocol_listener=RoborockDataProtocol.STATE,
),
RoborockSensorDescription(
key="cleaning_area",
Expand All @@ -145,13 +152,15 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None:
value_fn=lambda data: data.status.error_code_name,
entity_category=EntityCategory.DIAGNOSTIC,
options=RoborockErrorCode.keys(),
protocol_listener=RoborockDataProtocol.ERROR_CODE,
),
RoborockSensorDescription(
key="battery",
value_fn=lambda data: data.status.battery,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
protocol_listener=RoborockDataProtocol.BATTERY,
),
RoborockSensorDescription(
key="last_clean_start",
Expand Down Expand Up @@ -238,6 +247,8 @@ def __init__(
"""Initialize the entity."""
super().__init__(unique_id, coordinator)
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
def native_value(self) -> StateType | datetime.datetime:
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/roborock/vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any

from roborock.code_mappings import RoborockStateCode
from roborock.roborock_message import RoborockDataProtocol
from roborock.roborock_typing import RoborockCommand

from homeassistant.components.vacuum import (
Expand Down Expand Up @@ -94,6 +95,12 @@ def __init__(
StateVacuumEntity.__init__(self)
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
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
def state(self) -> str | None:
Expand Down
44 changes: 44 additions & 0 deletions tests/components/roborock/test_sensor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
"""Test Roborock Sensors."""
from unittest.mock import patch

from roborock import DeviceData, HomeDataDevice
from roborock.cloud_api import RoborockMqttClient
from roborock.const import (
FILTER_REPLACE_TIME,
MAIN_BRUSH_REPLACE_TIME,
SENSOR_DIRTY_REPLACE_TIME,
SIDE_BRUSH_REPLACE_TIME,
)
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol

from homeassistant.core import HomeAssistant

from .mock_data import CONSUMABLE, STATUS, USER_DATA

from tests.common import MockConfigEntry


Expand Down Expand Up @@ -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
== "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
)

0 comments on commit 6ef194f

Please sign in to comment.