From b54fe14a10a7a0ee3762c217c32cc6582ace406c Mon Sep 17 00:00:00 2001 From: Aaron Bach <bachya1208@gmail.com> Date: Tue, 12 Jul 2022 12:53:21 -0600 Subject: [PATCH] Replace Guardian `reboot` and `reset_valve_diagnostics` services with buttons (#75028) --- .coveragerc | 1 + homeassistant/components/guardian/__init__.py | 96 +++++++++++---- homeassistant/components/guardian/button.py | 113 ++++++++++++++++++ 3 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/guardian/button.py diff --git a/.coveragerc b/.coveragerc index 4e252fb9c92..94b9eceb11b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -450,6 +450,7 @@ omit = homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py homeassistant/components/guardian/binary_sensor.py + homeassistant/components/guardian/button.py homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 3707bf9d2eb..f13ca1a7ff5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -88,8 +88,7 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( }, ) - -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] @callback @@ -106,6 +105,25 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No client for device ID: {device_id}") +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str, +) -> None: + """Log a warning about a deprecated service call.""" + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in a future version; ' + 'use the "%s" service and pass it a target entity ID of "%s"' + ), + f"{call.domain}.{call.service}", + alternate_service, + alternate_target, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) @@ -164,17 +182,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback - def extract_client(func: Callable) -> Callable: - """Define a decorator to get the correct client for a service call.""" + def hydrate_with_entry_and_client(func: Callable) -> Callable: + """Define a decorator to hydrate a method with args based on service call.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + entry = hass.config_entries.async_get_entry(entry_id) + assert entry try: async with client: - await func(call, client) + await func(call, entry, client) except GuardianError as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -182,48 +202,76 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @extract_client - async def async_disable_ap(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_disable_ap( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Disable the onboard AP.""" await client.wifi.disable_ap() - @extract_client - async def async_enable_ap(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_enable_ap( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Enable the onboard AP.""" await client.wifi.enable_ap() - @extract_client - async def async_pair_sensor(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_pair_sensor( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Add a new paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER + ] uid = call.data[CONF_UID] await client.sensor.pair_sensor(uid) await paired_sensor_manager.async_pair_sensor(uid) - @extract_client - async def async_reboot(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_reboot( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Reboot the valve controller.""" + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reboot", + ) await client.system.reboot() - @extract_client - async def async_reset_valve_diagnostics(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_reset_valve_diagnostics( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Fully reset system motor diagnostics.""" + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reset_valve_diagnostics", + ) await client.valve.reset() - @extract_client - async def async_unpair_sensor(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_unpair_sensor( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Remove a paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER + ] uid = call.data[CONF_UID] await client.sensor.unpair_sensor(uid) await paired_sensor_manager.async_unpair_sensor(uid) - @extract_client - async def async_upgrade_firmware(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_upgrade_firmware( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Upgrade the device firmware.""" await client.system.upgrade_firmware( url=call.data[CONF_URL], @@ -389,7 +437,6 @@ class GuardianEntity(CoordinatorEntity): This should be extended by Guardian platforms. """ - raise NotImplementedError class PairedSensorEntity(GuardianEntity): @@ -454,7 +501,6 @@ class ValveControllerEntity(GuardianEntity): This should be extended by Guardian platforms. """ - raise NotImplementedError @callback def async_add_coordinator_update_listener(self, api: str) -> None: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py new file mode 100644 index 00000000000..e7cc757d367 --- /dev/null +++ b/homeassistant/components/guardian/button.py @@ -0,0 +1,113 @@ +"""Buttons for the Elexa Guardian integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from aioguardian import Client +from aioguardian.errors import GuardianError + +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.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import ValveControllerEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +@dataclass +class GuardianButtonDescriptionMixin: + """Define an entity description mixin for Guardian buttons.""" + + push_action: Callable[[Client], Awaitable] + + +@dataclass +class GuardianButtonDescription( + ButtonEntityDescription, GuardianButtonDescriptionMixin +): + """Describe a Guardian button description.""" + + +BUTTON_KIND_REBOOT = "reboot" +BUTTON_KIND_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" + + +async def _async_reboot(client: Client) -> None: + """Reboot the Guardian.""" + await client.system.reboot() + + +async def _async_valve_reset(client: Client) -> None: + """Reset the valve diagnostics on the Guardian.""" + await client.valve.reset() + + +BUTTON_DESCRIPTIONS = ( + GuardianButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + push_action=_async_reboot, + ), + GuardianButtonDescription( + key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, + name="Reset valve diagnostics", + push_action=_async_valve_reset, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Guardian buttons based on a config entry.""" + async_add_entities( + [ + GuardianButton( + entry, + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], + description, + ) + for description in BUTTON_DESCRIPTIONS + ] + ) + + +class GuardianButton(ValveControllerEntity, ButtonEntity): + """Define a Guardian button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: GuardianButtonDescription + + def __init__( + self, + entry: ConfigEntry, + client: Client, + coordinators: dict[str, DataUpdateCoordinator], + description: GuardianButtonDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinators, description) + + self._client = client + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + async with self._client: + await self.entity_description.push_action(self._client) + except GuardianError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err -- GitLab