Add time platform to Roborock (#94039)

pull/97210/head
Luke 2023-07-25 05:29:48 -06:00 committed by GitHub
parent 6b41c324cc
commit 6c43ce69d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 198 additions and 0 deletions

View File

@ -11,5 +11,6 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,
Platform.NUMBER,
]

View File

@ -154,6 +154,14 @@
"name": "Status indicator light"
}
},
"time": {
"dnd_start_time": {
"name": "Do not disturb begin"
},
"dnd_end_time": {
"name": "Do not disturb end"
}
},
"vacuum": {
"roborock": {
"state_attributes": {

View File

@ -0,0 +1,150 @@
"""Support for Roborock time."""
import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import datetime
from datetime import time
import logging
from typing import Any
from roborock.api import AttributeCache
from roborock.command_cache import CacheableAttribute
from roborock.exceptions import RoborockException
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockEntity
_LOGGER = logging.getLogger(__name__)
@dataclass
class RoborockTimeDescriptionMixin:
"""Define an entity description mixin for time entities."""
# Gets the status of the switch
cache_key: CacheableAttribute
# Sets the status of the switch
update_value: Callable[[AttributeCache, datetime.time], Coroutine[Any, Any, dict]]
# Attribute from cache
get_value: Callable[[AttributeCache], datetime.time]
@dataclass
class RoborockTimeDescription(TimeEntityDescription, RoborockTimeDescriptionMixin):
"""Class to describe an Roborock time entity."""
TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
RoborockTimeDescription(
key="dnd_start_time",
translation_key="dnd_start_time",
icon="mdi:bell-cancel",
cache_key=CacheableAttribute.dnd_timer,
update_value=lambda cache, desired_time: cache.update_value(
[
desired_time.hour,
desired_time.minute,
cache.value.get("end_hour"),
cache.value.get("end_minute"),
]
),
get_value=lambda cache: datetime.time(
hour=cache.value.get("start_hour"), minute=cache.value.get("start_minute")
),
entity_category=EntityCategory.CONFIG,
),
RoborockTimeDescription(
key="dnd_end_time",
translation_key="dnd_end_time",
icon="mdi:bell-ring",
cache_key=CacheableAttribute.dnd_timer,
update_value=lambda cache, desired_time: cache.update_value(
[
cache.value.get("start_hour"),
cache.value.get("start_minute"),
desired_time.hour,
desired_time.minute,
]
),
get_value=lambda cache: datetime.time(
hour=cache.value.get("end_hour"), minute=cache.value.get("end_minute")
),
entity_category=EntityCategory.CONFIG,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock time platform."""
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
possible_entities: list[
tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription]
] = [
(coordinator, description)
for coordinator in coordinators.values()
for description in TIME_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
results = await asyncio.gather(
*(
coordinator.api.cache.get(description.cache_key).async_value()
for coordinator, description in possible_entities
),
return_exceptions=True,
)
valid_entities: list[RoborockTimeEntity] = []
for (coordinator, description), result in zip(possible_entities, results):
if result is None or isinstance(result, RoborockException):
_LOGGER.debug("Not adding entity because of %s", result)
else:
valid_entities.append(
RoborockTimeEntity(
f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}",
coordinator,
description,
)
)
async_add_entities(valid_entities)
class RoborockTimeEntity(RoborockEntity, TimeEntity):
"""A class to let you set options on a Roborock vacuum where the potential options are fixed."""
entity_description: RoborockTimeDescription
def __init__(
self,
unique_id: str,
coordinator: RoborockDataUpdateCoordinator,
entity_description: RoborockTimeDescription,
) -> None:
"""Create a time entity."""
self.entity_description = entity_description
super().__init__(unique_id, coordinator.device_info, coordinator.api)
@property
def native_value(self) -> time | None:
"""Return the value reported by the time."""
return self.entity_description.get_value(
self.get_cache(self.entity_description.cache_key)
)
async def async_set_value(self, value: time) -> None:
"""Set the time."""
await self.entity_description.update_value(
self.get_cache(self.entity_description.cache_key), value
)

View File

@ -0,0 +1,39 @@
"""Test Roborock Time platform."""
from datetime import time
from unittest.mock import patch
import pytest
from homeassistant.components.time import SERVICE_SET_VALUE
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
("entity_id"),
[
("time.roborock_s7_maxv_do_not_disturb_begin"),
("time.roborock_s7_maxv_do_not_disturb_end"),
],
)
async def test_update_success(
hass: HomeAssistant,
bypass_api_fixture,
setup_entry: MockConfigEntry,
entity_id: str,
) -> None:
"""Test turning switch entities on and off."""
# Ensure that the entity exist, as these test can pass even if there is no entity.
assert hass.states.get(entity_id) is not None
with patch(
"homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message"
) as mock_send_message:
await hass.services.async_call(
"time",
SERVICE_SET_VALUE,
service_data={"time": time(hour=1, minute=1)},
blocking=True,
target={"entity_id": entity_id},
)
assert mock_send_message.assert_called_once