From ec8988f8ea9186309ab9aeecee298a83e94403c3 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 27 Jun 2023 14:25:29 -0600 Subject: [PATCH] Add time entity for sleep mode start time to Litter-Robot 3 (#94194) --- .../components/litterrobot/__init__.py | 2 +- .../components/litterrobot/strings.json | 5 ++ homeassistant/components/litterrobot/time.py | 82 +++++++++++++++++++ tests/components/litterrobot/test_time.py | 35 ++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/litterrobot/time.py create mode 100644 tests/components/litterrobot/test_time.py diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 45483f99e5b0e..c7eda2f118b98 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -18,7 +18,7 @@ Platform.SWITCH, ), LitterRobot: (Platform.VACUUM,), - LitterRobot3: (Platform.BUTTON,), + LitterRobot3: (Platform.BUTTON, Platform.TIME), LitterRobot4: (Platform.UPDATE,), FeederRobot: (Platform.BUTTON,), } diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index c136cd7f6855d..c0ba266659307 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -128,6 +128,11 @@ "name": "Panel lockout" } }, + "time": { + "sleep_mode_start_time": { + "name": "Sleep mode start time" + } + }, "vacuum": { "litter_box": { "name": "Litter box" diff --git a/homeassistant/components/litterrobot/time.py b/homeassistant/components/litterrobot/time.py new file mode 100644 index 0000000000000..f352b7cee70cc --- /dev/null +++ b/homeassistant/components/litterrobot/time.py @@ -0,0 +1,82 @@ +"""Support for Litter-Robot time.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from datetime import datetime, time +from typing import Any, Generic + +from pylitterbot import LitterRobot3 + +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 +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .entity import LitterRobotEntity, _RobotT +from .hub import LitterRobotHub + + +@dataclass +class RequiredKeysMixin(Generic[_RobotT]): + """A class that describes robot time entity required keys.""" + + value_fn: Callable[[_RobotT], time | None] + set_fn: Callable[[_RobotT, time], Coroutine[Any, Any, bool]] + + +@dataclass +class RobotTimeEntityDescription(TimeEntityDescription, RequiredKeysMixin[_RobotT]): + """A class that describes robot time entities.""" + + +def _as_local_time(start: datetime | None) -> time | None: + """Return a datetime as local time.""" + return dt_util.as_local(start).time() if start else None + + +LITTER_ROBOT_3_SLEEP_START = RobotTimeEntityDescription[LitterRobot3]( + key="sleep_mode_start_time", + translation_key="sleep_mode_start_time", + entity_category=EntityCategory.CONFIG, + value_fn=lambda robot: _as_local_time(robot.sleep_mode_start_time), + set_fn=lambda robot, value: robot.set_sleep_mode( + robot.sleep_mode_enabled, value.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Litter-Robot cleaner using config entry.""" + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + LitterRobotTimeEntity( + robot=robot, hub=hub, description=LITTER_ROBOT_3_SLEEP_START + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) + ] + ) + + +class LitterRobotTimeEntity(LitterRobotEntity[_RobotT], TimeEntity): + """Litter-Robot time entity.""" + + entity_description: RobotTimeEntityDescription[_RobotT] + + @property + def native_value(self) -> time | None: + """Return the value reported by the time.""" + return self.entity_description.value_fn(self.robot) + + async def async_set_value(self, value: time) -> None: + """Update the current value.""" + await self.entity_description.set_fn(self.robot, value) diff --git a/tests/components/litterrobot/test_time.py b/tests/components/litterrobot/test_time.py new file mode 100644 index 0000000000000..6532f2a3bc7b6 --- /dev/null +++ b/tests/components/litterrobot/test_time.py @@ -0,0 +1,35 @@ +"""Test the Litter-Robot time entity.""" +from __future__ import annotations + +from datetime import time +from unittest.mock import MagicMock + +from pylitterbot import LitterRobot3 + +from homeassistant.components.time import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import setup_integration + +SLEEP_START_TIME_ENTITY_ID = "time.test_sleep_mode_start_time" + + +async def test_sleep_mode_start_time( + hass: HomeAssistant, mock_account: MagicMock +) -> None: + """Tests the sleep mode start time.""" + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + + entity = hass.states.get(SLEEP_START_TIME_ENTITY_ID) + assert entity + assert entity.state == "17:16:00" + + robot: LitterRobot3 = mock_account.robots[0] + await hass.services.async_call( + PLATFORM_DOMAIN, + "set_value", + {ATTR_ENTITY_ID: SLEEP_START_TIME_ENTITY_ID, "time": time(23, 0)}, + blocking=True, + ) + robot.set_sleep_mode.assert_awaited_once_with(True, time(23, 0))