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