From 4bade86dccabdf1b758fa9f9f2ad853252946f3d Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Wed, 31 May 2023 16:00:52 +0100 Subject: [PATCH] Add time component to Melnor Bluetooth integration (#93652) * Add time component to Melnor Bluetooth integration * Apply suggestions from code review --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/melnor/__init__.py | 1 + homeassistant/components/melnor/manifest.json | 2 +- homeassistant/components/melnor/time.py | 91 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/melnor/test_time.py | 50 ++++++++++ 6 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/melnor/time.py create mode 100644 tests/components/melnor/test_time.py diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 3cd9fee4fe7..9a15e81dc22 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__init__.py @@ -18,6 +18,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, + Platform.TIME, ] diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index 8d5d86a8bf7..87a5583fa4f 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", - "requirements": ["melnor-bluetooth==0.0.21"] + "requirements": ["melnor-bluetooth==0.0.22"] } diff --git a/homeassistant/components/melnor/time.py b/homeassistant/components/melnor/time.py new file mode 100644 index 00000000000..7abdf62e20c --- /dev/null +++ b/homeassistant/components/melnor/time.py @@ -0,0 +1,91 @@ +"""Number support for Melnor Bluetooth water timer.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from datetime import time +from typing import Any + +from melnor_bluetooth.device import Valve + +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 .const import DOMAIN +from .models import ( + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) + + +@dataclass +class MelnorZoneTimeEntityDescriptionMixin: + """Mixin for required keys.""" + + set_time_fn: Callable[[Valve, time], Coroutine[Any, Any, None]] + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorZoneTimeEntityDescription( + TimeEntityDescription, MelnorZoneTimeEntityDescriptionMixin +): + """Describes Melnor number entity.""" + + +ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneTimeEntityDescription] = [ + MelnorZoneTimeEntityDescription( + entity_category=EntityCategory.CONFIG, + key="frequency_start_time", + name="Schedule Start Time", + set_time_fn=lambda valve, value: valve.set_frequency_start_time(value), + state_fn=lambda valve: valve.frequency.start_time, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the number platform.""" + + coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneTime(coordinator, description, valve), + ) + ) + + +class MelnorZoneTime(MelnorZoneEntity, TimeEntity): + """A time implementation for a melnor device.""" + + entity_description: MelnorZoneTimeEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorZoneTimeEntityDescription, + valve: Valve, + ) -> None: + """Initialize a number for a melnor device.""" + super().__init__(coordinator, entity_description, valve) + + @property + def native_value(self) -> time | None: + """Return the current value.""" + return self.entity_description.state_fn(self._valve) + + async def async_set_value(self, value: time) -> None: + """Update the current value.""" + await self.entity_description.set_time_fn(self._valve, value) diff --git a/requirements_all.txt b/requirements_all.txt index 92449d083b7..51293088a31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1122,7 +1122,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.21 +melnor-bluetooth==0.0.22 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dffbc65c812..032884adccc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.21 +melnor-bluetooth==0.0.22 # homeassistant.components.meteo_france meteofrance-api==1.2.0 diff --git a/tests/components/melnor/test_time.py b/tests/components/melnor/test_time.py new file mode 100644 index 00000000000..682f518d40b --- /dev/null +++ b/tests/components/melnor/test_time.py @@ -0,0 +1,50 @@ +"""Test the Melnor time platform.""" +from __future__ import annotations + +from datetime import time + +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from .conftest import ( + mock_config_entry, + patch_async_ble_device_from_address, + patch_async_register_callback, + patch_melnor_device, +) + +from tests.common import async_fire_time_changed + + +async def test_schedule_start_time(hass: HomeAssistant) -> None: + """Test the frequency schedule start time.""" + + now = dt_util.now() + + entry = mock_config_entry(hass) + + with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + device = device_patch.return_value + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + time_entity = hass.states.get("time.zone_1_schedule_start_time") + + assert time_entity is not None + assert time_entity.state == device.zone1.frequency.start_time.isoformat() + + await hass.services.async_call( + "time", + "set_value", + {"entity_id": "time.zone_1_schedule_start_time", "time": time(1, 0)}, + blocking=True, + ) + + async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10)) + await hass.async_block_till_done() + + time_entity = hass.states.get("time.zone_1_schedule_start_time") + + assert time_entity is not None + assert time_entity.state == time(1, 0).isoformat()