Add reboot button to RainMachine (#75227)

This commit is contained in:
Aaron Bach 2022-08-04 13:22:10 -06:00 committed by GitHub
parent 0df4642b62
commit 3d42c4ca87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 212 additions and 36 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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": {

View file

@ -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)

View file

@ -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

View file

@ -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