Add reboot button to RainMachine (#75227)
This commit is contained in:
parent
0df4642b62
commit
3d42c4ca87
7 changed files with 212 additions and 36 deletions
|
@ -976,6 +976,7 @@ omit =
|
||||||
homeassistant/components/raincloud/*
|
homeassistant/components/raincloud/*
|
||||||
homeassistant/components/rainmachine/__init__.py
|
homeassistant/components/rainmachine/__init__.py
|
||||||
homeassistant/components/rainmachine/binary_sensor.py
|
homeassistant/components/rainmachine/binary_sensor.py
|
||||||
|
homeassistant/components/rainmachine/button.py
|
||||||
homeassistant/components/rainmachine/model.py
|
homeassistant/components/rainmachine/model.py
|
||||||
homeassistant/components/rainmachine/sensor.py
|
homeassistant/components/rainmachine/sensor.py
|
||||||
homeassistant/components/rainmachine/switch.py
|
homeassistant/components/rainmachine/switch.py
|
||||||
|
|
|
@ -30,11 +30,7 @@ from homeassistant.helpers import (
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
UpdateFailed,
|
|
||||||
)
|
|
||||||
from homeassistant.util.network import is_ip_address
|
from homeassistant.util.network import is_ip_address
|
||||||
|
|
||||||
from .config_flow import get_client_controller
|
from .config_flow import get_client_controller
|
||||||
|
@ -49,20 +45,13 @@ from .const import (
|
||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
from .model import RainMachineEntityDescription
|
from .model import RainMachineEntityDescription
|
||||||
|
from .util import RainMachineDataUpdateCoordinator
|
||||||
|
|
||||||
DEFAULT_SSL = True
|
DEFAULT_SSL = True
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, 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),
|
|
||||||
}
|
|
||||||
|
|
||||||
CONF_CONDITION = "condition"
|
CONF_CONDITION = "condition"
|
||||||
CONF_DEWPOINT = "dewpoint"
|
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
|
@dataclass
|
||||||
class RainMachineData:
|
class RainMachineData:
|
||||||
"""Define an object to be stored in `hass.data`."""
|
"""Define an object to be stored in `hass.data`."""
|
||||||
|
|
||||||
controller: Controller
|
controller: Controller
|
||||||
coordinators: dict[str, DataUpdateCoordinator]
|
coordinators: dict[str, RainMachineDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -233,24 +230,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|
||||||
return data
|
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 = []
|
controller_init_tasks = []
|
||||||
coordinators = {}
|
coordinators = {}
|
||||||
|
for api_category, update_interval in COORDINATOR_UPDATE_INTERVAL_MAP.items():
|
||||||
for api_category in (
|
coordinator = coordinators[api_category] = RainMachineDataUpdateCoordinator(
|
||||||
DATA_PROGRAMS,
|
|
||||||
DATA_PROVISION_SETTINGS,
|
|
||||||
DATA_RESTRICTIONS_CURRENT,
|
|
||||||
DATA_RESTRICTIONS_UNIVERSAL,
|
|
||||||
DATA_ZONES,
|
|
||||||
):
|
|
||||||
coordinator = coordinators[api_category] = DataUpdateCoordinator(
|
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
entry=entry,
|
||||||
name=f'{controller.name} ("{api_category}")',
|
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),
|
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)
|
await asyncio.gather(*controller_init_tasks)
|
||||||
|
|
||||||
|
@ -439,12 +437,6 @@ class RainMachineEntity(CoordinatorEntity):
|
||||||
self.update_from_latest_data()
|
self.update_from_latest_data()
|
||||||
self.async_write_ha_state()
|
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
|
@callback
|
||||||
def update_from_latest_data(self) -> None:
|
def update_from_latest_data(self) -> None:
|
||||||
"""Update the state."""
|
"""Update the state."""
|
||||||
raise NotImplementedError
|
|
||||||
|
|
90
homeassistant/components/rainmachine/button.py
Normal file
90
homeassistant/components/rainmachine/button.py
Normal 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)
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "RainMachine",
|
"name": "RainMachine",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||||
"requirements": ["regenmaschine==2022.07.1"],
|
"requirements": ["regenmaschine==2022.07.3"],
|
||||||
"codeowners": ["@bachya"],
|
"codeowners": ["@bachya"],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"homekit": {
|
"homekit": {
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
"""Define RainMachine utilities."""
|
"""Define RainMachine utilities."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.backports.enum import StrEnum
|
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):
|
class RunStates(StrEnum):
|
||||||
|
@ -29,3 +43,82 @@ def key_exists(data: dict[str, Any], search_key: str) -> bool:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return key_exists(value, search_key)
|
return key_exists(value, search_key)
|
||||||
return False
|
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)
|
||||||
|
|
|
@ -2081,7 +2081,7 @@ raincloudy==0.0.7
|
||||||
raspyrfm-client==1.2.8
|
raspyrfm-client==1.2.8
|
||||||
|
|
||||||
# homeassistant.components.rainmachine
|
# homeassistant.components.rainmachine
|
||||||
regenmaschine==2022.07.1
|
regenmaschine==2022.07.3
|
||||||
|
|
||||||
# homeassistant.components.renault
|
# homeassistant.components.renault
|
||||||
renault-api==0.1.11
|
renault-api==0.1.11
|
||||||
|
|
|
@ -1402,7 +1402,7 @@ radios==0.1.1
|
||||||
radiotherm==2.1.0
|
radiotherm==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.rainmachine
|
# homeassistant.components.rainmachine
|
||||||
regenmaschine==2022.07.1
|
regenmaschine==2022.07.3
|
||||||
|
|
||||||
# homeassistant.components.renault
|
# homeassistant.components.renault
|
||||||
renault-api==0.1.11
|
renault-api==0.1.11
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue