diff --git a/.coveragerc b/.coveragerc index 341494b14245416f64a0eb9eca30263f582170f0..20c6fd2c60e1edda16f1681c56b4bc61e83582b6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -976,6 +976,7 @@ omit = homeassistant/components/raincloud/* homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/binary_sensor.py + homeassistant/components/rainmachine/button.py homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c30ce81dc6dae9198823b0fc49164682d2572f8d..dccdaaba74c136f2b8a6882c8eb5b05dfe915c06 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -30,11 +30,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller @@ -49,20 +45,13 @@ from .const import ( LOGGER, ) from .model import RainMachineEntityDescription +from .util import RainMachineDataUpdateCoordinator DEFAULT_SSL = True CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] - -UPDATE_INTERVALS = { - DATA_PROVISION_SETTINGS: timedelta(minutes=1), - DATA_PROGRAMS: timedelta(seconds=30), - DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), - DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), - DATA_ZONES: timedelta(seconds=15), -} +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -134,13 +123,21 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( } ) +COORDINATOR_UPDATE_INTERVAL_MAP = { + DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_PROGRAMS: timedelta(seconds=30), + DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), + DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), + DATA_ZONES: timedelta(seconds=15), +} + @dataclass class RainMachineData: """Define an object to be stored in `hass.data`.""" controller: Controller - coordinators: dict[str, DataUpdateCoordinator] + coordinators: dict[str, RainMachineDataUpdateCoordinator] @callback @@ -233,24 +230,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data + async def async_init_coordinator( + coordinator: RainMachineDataUpdateCoordinator, + ) -> None: + """Initialize a RainMachineDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + controller_init_tasks = [] coordinators = {} - - for api_category in ( - DATA_PROGRAMS, - DATA_PROVISION_SETTINGS, - DATA_RESTRICTIONS_CURRENT, - DATA_RESTRICTIONS_UNIVERSAL, - DATA_ZONES, - ): - coordinator = coordinators[api_category] = DataUpdateCoordinator( + for api_category, update_interval in COORDINATOR_UPDATE_INTERVAL_MAP.items(): + coordinator = coordinators[api_category] = RainMachineDataUpdateCoordinator( hass, - LOGGER, + entry=entry, name=f'{controller.name} ("{api_category}")', - update_interval=UPDATE_INTERVALS[api_category], + api_category=api_category, + update_interval=update_interval, update_method=partial(async_update, api_category), ) - controller_init_tasks.append(coordinator.async_refresh()) + controller_init_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*controller_init_tasks) @@ -439,12 +437,6 @@ class RainMachineEntity(CoordinatorEntity): self.update_from_latest_data() self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self.update_from_latest_data() - @callback def update_from_latest_data(self) -> None: """Update the state.""" - raise NotImplementedError diff --git a/homeassistant/components/rainmachine/button.py b/homeassistant/components/rainmachine/button.py new file mode 100644 index 0000000000000000000000000000000000000000..14bfb8786422af443a177fe7e9c7016fb7ac0a6f --- /dev/null +++ b/homeassistant/components/rainmachine/button.py @@ -0,0 +1,90 @@ +"""Buttons for the RainMachine integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from regenmaschine.controller import Controller +from regenmaschine.errors import RainMachineError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_PROVISION_SETTINGS, DOMAIN +from .model import RainMachineEntityDescription + + +@dataclass +class RainMachineButtonDescriptionMixin: + """Define an entity description mixin for RainMachine buttons.""" + + push_action: Callable[[Controller], Awaitable] + + +@dataclass +class RainMachineButtonDescription( + ButtonEntityDescription, + RainMachineEntityDescription, + RainMachineButtonDescriptionMixin, +): + """Describe a RainMachine button description.""" + + +BUTTON_KIND_REBOOT = "reboot" + + +async def _async_reboot(controller: Controller) -> None: + """Reboot the RainMachine.""" + await controller.machine.reboot() + + +BUTTON_DESCRIPTIONS = ( + RainMachineButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + api_category=DATA_PROVISION_SETTINGS, + push_action=_async_reboot, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up RainMachine buttons based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + RainMachineButton(entry, data, description) + for description in BUTTON_DESCRIPTIONS + ) + + +class RainMachineButton(RainMachineEntity, ButtonEntity): + """Define a RainMachine button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: RainMachineButtonDescription + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + await self.entity_description.push_action(self._data.controller) + except RainMachineError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b318ef7f295e83e4f165de09c92de64afb49cc7f..4d60730ba6cde8db38f34b1a70e806e69702dd08 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.1"], + "requirements": ["regenmaschine==2022.07.3"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 6bf15f2fb9c5dd417c96d42e44e9c6d1c6cbc6fe..dc772690ec54163644685c3b57950778eb4f3a51 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -1,9 +1,23 @@ """Define RainMachine utilities.""" from __future__ import annotations +from collections.abc import Awaitable, Callable +from datetime import timedelta from typing import Any from homeassistant.backports.enum import StrEnum +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import LOGGER + +SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" +SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" class RunStates(StrEnum): @@ -29,3 +43,82 @@ def key_exists(data: dict[str, Any], search_key: str) -> bool: if isinstance(value, dict): return key_exists(value, search_key) return False + + +class RainMachineDataUpdateCoordinator(DataUpdateCoordinator[dict]): + """Define an extended DataUpdateCoordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + *, + entry: ConfigEntry, + name: str, + api_category: str, + update_interval: timedelta, + update_method: Callable[..., Awaitable], + ) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=name, + update_interval=update_interval, + update_method=update_method, + ) + + self._rebooting = False + self._signal_handler_unsubs: list[Callable[..., None]] = [] + self.config_entry = entry + self.signal_reboot_completed = SIGNAL_REBOOT_COMPLETED.format( + self.config_entry.entry_id + ) + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_completed() -> None: + """Respond to a reboot completed notification.""" + LOGGER.debug("%s responding to reboot complete", self.name) + self._rebooting = False + self.last_update_success = True + self.async_update_listeners() + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + LOGGER.debug("%s responding to reboot request", self.name) + self._rebooting = True + self.last_update_success = False + self.async_update_listeners() + + for signal, func in ( + (self.signal_reboot_completed, async_reboot_completed), + (self.signal_reboot_requested, async_reboot_requested), + ): + self._signal_handler_unsubs.append( + async_dispatcher_connect(self.hass, signal, func) + ) + + @callback + def async_check_reboot_complete() -> None: + """Check whether an active reboot has been completed.""" + if self._rebooting and self.last_update_success: + LOGGER.debug("%s discovered reboot complete", self.name) + async_dispatcher_send(self.hass, self.signal_reboot_completed) + + self.async_add_listener(async_check_reboot_complete) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) diff --git a/requirements_all.txt b/requirements_all.txt index c1badeb18deb9849315e5e2a3a1ec39564a5dc31..3abb34ff5dad9f6b933ca9f42579c343d4ffbb0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2081,7 +2081,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.1 +regenmaschine==2022.07.3 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ef2f0ff1b037240875e60562d0dfb64e1205e96..96845b12579c9b51eee13e6f649d94ee2a9e45f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1402,7 +1402,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.1 +regenmaschine==2022.07.3 # homeassistant.components.renault renault-api==0.1.11