diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 0edd7088b89..892080b9afe 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -2,14 +2,23 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable +from collections.abc import Awaitable, Callable from typing import cast from aioguardian import Client +from aioguardian.errors import GuardianError +import voluptuous as vol -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from homeassistant.core import HomeAssistant, callback +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_FILENAME, + CONF_IP_ADDRESS, + CONF_PORT, + CONF_URL, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -28,16 +37,66 @@ from .const import ( DATA_CLIENT, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, - DATA_PAIRED_SENSOR_MANAGER, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) from .util import GuardianDataUpdateCoordinator +DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" + +SERVICE_NAME_DISABLE_AP = "disable_ap" +SERVICE_NAME_ENABLE_AP = "enable_ap" +SERVICE_NAME_PAIR_SENSOR = "pair_sensor" +SERVICE_NAME_REBOOT = "reboot" +SERVICE_NAME_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" +SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" +SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" + +SERVICES = ( + SERVICE_NAME_DISABLE_AP, + SERVICE_NAME_ENABLE_AP, + SERVICE_NAME_PAIR_SENSOR, + SERVICE_NAME_REBOOT, + SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, + SERVICE_NAME_UNPAIR_SENSOR, + SERVICE_NAME_UPGRADE_FIRMWARE, +) + +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_UID): cv.string, + } +) + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, +) + + PLATFORMS = ["binary_sensor", "sensor", "switch"] +@callback +def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: + """Get the entry ID related to a service call (by device ID).""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + + if device_entry := device_registry.async_get(device_id): + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.entry_id in device_entry.config_entries: + return entry.entry_id + + raise ValueError(f"No client for device ID: {device_id}") + + 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]) @@ -95,6 +154,97 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up all of the Guardian entity platforms: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + @callback + def extract_client(func: Callable) -> Callable: + """Define a decorator to get the correct client for a 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] + + try: + async with client: + await func(call, client) + except GuardianError as err: + LOGGER.error("Error while executing %s: %s", func.__name__, err) + + return wrapper + + @extract_client + async def async_disable_ap(call: ServiceCall, client: Client) -> None: + """Disable the onboard AP.""" + await client.wifi.disable_ap() + + @extract_client + async def async_enable_ap(call: ServiceCall, client: Client) -> None: + """Enable the onboard AP.""" + await client.wifi.enable_ap() + + @extract_client + async def async_pair_sensor(call: ServiceCall, 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] + 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: + """Reboot the valve controller.""" + await client.system.reboot() + + @extract_client + async def async_reset_valve_diagnostics(call: ServiceCall, client: Client) -> None: + """Fully reset system motor diagnostics.""" + await client.valve.reset() + + @extract_client + async def async_unpair_sensor(call: ServiceCall, 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] + 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: + """Upgrade the device firmware.""" + await client.system.upgrade_firmware( + url=call.data[CONF_URL], + port=call.data[CONF_PORT], + filename=call.data[CONF_FILENAME], + ) + + for service_name, schema, method in ( + (SERVICE_NAME_DISABLE_AP, None, async_disable_ap), + (SERVICE_NAME_ENABLE_AP, None, async_enable_ap), + ( + SERVICE_NAME_PAIR_SENSOR, + SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, + async_pair_sensor, + ), + (SERVICE_NAME_REBOOT, None, async_reboot), + (SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, None, async_reset_valve_diagnostics), + ( + SERVICE_NAME_UNPAIR_SENSOR, + SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, + async_unpair_sensor, + ), + ( + SERVICE_NAME_UPGRADE_FIRMWARE, + SERVICE_UPGRADE_FIRMWARE_SCHEMA, + async_upgrade_firmware, + ), + ): + if hass.services.has_service(DOMAIN, service_name): + continue + hass.services.async_register(DOMAIN, service_name, method, schema=schema) + return True @@ -104,6 +254,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of Guardian, deregister any services + # defined during integration setup: + for service_name in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + return unload_ok diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 5e3779cc447..3499db24c03 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -17,6 +17,5 @@ CONF_UID = "uid" DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" -DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index cb2e7827657..4d48783c955 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -2,25 +2,36 @@ disable_ap: name: Disable AP description: Disable the device's onboard access point. - target: - entity: - integration: guardian - domain: switch + fields: + device_id: + name: Valve Controller + description: The valve controller whose AP should be disabled + required: true + selector: + device: + integration: guardian enable_ap: name: Enable AP description: Enable the device's onboard access point. - target: - entity: - integration: guardian - domain: switch -pair_sensor: - name: Pair sensor - description: Add a new paired sensor to the valve controller. - target: - entity: - integration: guardian - domain: switch fields: + device_id: + name: Valve Controller + description: The valve controller whose AP should be enabled + required: true + selector: + device: + integration: guardian +pair_sensor: + name: Pair Sensor + description: Add a new paired sensor to the valve controller. + fields: + device_id: + name: Valve Controller + description: The valve controller to add the sensor to + required: true + selector: + device: + integration: guardian uid: name: UID description: The UID of the paired sensor @@ -31,25 +42,36 @@ pair_sensor: reboot: name: Reboot description: Reboot the device. - target: - entity: - integration: guardian - domain: switch -reset_valve_diagnostics: - name: Reset valve diagnostics - description: Fully (and irrecoverably) reset all valve diagnostics. - target: - entity: - integration: guardian - domain: switch -unpair_sensor: - name: Unpair sensor - description: Remove a paired sensor from the valve controller. - target: - entity: - integration: guardian - domain: switch fields: + device_id: + name: Valve Controller + description: The valve controller to reboot + required: true + selector: + device: + integration: guardian +reset_valve_diagnostics: + name: Reset Valve Diagnostics + description: Fully (and irrecoverably) reset all valve diagnostics. + fields: + device_id: + name: Valve Controller + description: The valve controller whose diagnostics should be reset + required: true + selector: + device: + integration: guardian +unpair_sensor: + name: Unpair Sensor + description: Remove a paired sensor from the valve controller. + fields: + device_id: + name: Valve Controller + description: The valve controller to remove the sensor from + required: true + selector: + device: + integration: guardian uid: name: UID description: The UID of the paired sensor @@ -60,11 +82,14 @@ unpair_sensor: upgrade_firmware: name: Upgrade firmware description: Upgrade the device firmware. - target: - entity: - integration: guardian - domain: switch fields: + device_id: + name: Valve Controller + description: The valve controller whose firmware should be upgraded + required: true + selector: + device: + integration: guardian url: name: URL description: The URL of the server hosting the firmware file. @@ -76,7 +101,9 @@ upgrade_firmware: description: The port on which the firmware file is served. example: 443 selector: - text: + number: + min: 1 + max: 65535 filename: name: Filename description: The firmware filename. diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 7417499e53a..17e28c1901a 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -5,40 +5,21 @@ from typing import Any from aioguardian import Client from aioguardian.errors import GuardianError -import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FILENAME, CONF_PORT, CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ValveControllerEntity -from .const import ( - API_VALVE_STATUS, - CONF_UID, - DATA_CLIENT, - DATA_COORDINATOR, - DATA_PAIRED_SENSOR_MANAGER, - DOMAIN, - LOGGER, -) +from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN, LOGGER ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" ATTR_TRAVEL_COUNT = "travel_count" -SERVICE_DISABLE_AP = "disable_ap" -SERVICE_ENABLE_AP = "enable_ap" -SERVICE_PAIR_SENSOR = "pair_sensor" -SERVICE_REBOOT = "reboot" -SERVICE_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" -SERVICE_UNPAIR_SENSOR = "unpair_sensor" -SERVICE_UPGRADE_FIRMWARE = "upgrade_firmware" - SWITCH_KIND_VALVE = "valve" SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( @@ -52,31 +33,6 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - platform = entity_platform.async_get_current_platform() - - for service_name, schema, method in ( - (SERVICE_DISABLE_AP, {}, "async_disable_ap"), - (SERVICE_ENABLE_AP, {}, "async_enable_ap"), - (SERVICE_PAIR_SENSOR, {vol.Required(CONF_UID): cv.string}, "async_pair_sensor"), - (SERVICE_REBOOT, {}, "async_reboot"), - (SERVICE_RESET_VALVE_DIAGNOSTICS, {}, "async_reset_valve_diagnostics"), - ( - SERVICE_UPGRADE_FIRMWARE, - { - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, - "async_upgrade_firmware", - ), - ( - SERVICE_UNPAIR_SENSOR, - {vol.Required(CONF_UID): cv.string}, - "async_unpair_sensor", - ), - ): - platform.async_register_entity_service(service_name, schema, method) - async_add_entities( [ ValveControllerSwitch( @@ -135,78 +91,6 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): } ) - async def async_disable_ap(self) -> None: - """Disable the device's onboard access point.""" - try: - async with self._client: - await self._client.wifi.disable_ap() - except GuardianError as err: - LOGGER.error("Error while disabling valve controller AP: %s", err) - - async def async_enable_ap(self) -> None: - """Enable the device's onboard access point.""" - try: - async with self._client: - await self._client.wifi.enable_ap() - except GuardianError as err: - LOGGER.error("Error while enabling valve controller AP: %s", err) - - async def async_pair_sensor(self, *, uid: str) -> None: - """Add a new paired sensor.""" - try: - async with self._client: - await self._client.sensor.pair_sensor(uid) - except GuardianError as err: - LOGGER.error("Error while adding paired sensor: %s", err) - return - - await self.hass.data[DOMAIN][self._entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ].async_pair_sensor(uid) - - async def async_reboot(self) -> None: - """Reboot the device.""" - try: - async with self._client: - await self._client.system.reboot() - except GuardianError as err: - LOGGER.error("Error while rebooting valve controller: %s", err) - - async def async_reset_valve_diagnostics(self) -> None: - """Fully reset system motor diagnostics.""" - try: - async with self._client: - await self._client.valve.reset() - except GuardianError as err: - LOGGER.error("Error while resetting valve diagnostics: %s", err) - - async def async_unpair_sensor(self, *, uid: str) -> None: - """Add a new paired sensor.""" - try: - async with self._client: - await self._client.sensor.unpair_sensor(uid) - except GuardianError as err: - LOGGER.error("Error while removing paired sensor: %s", err) - return - - await self.hass.data[DOMAIN][self._entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ].async_unpair_sensor(uid) - - async def async_upgrade_firmware( - self, *, url: str, port: int, filename: str - ) -> None: - """Upgrade the device firmware.""" - try: - async with self._client: - await self._client.system.upgrade_firmware( - url=url, - port=port, - filename=filename, - ) - except GuardianError as err: - LOGGER.error("Error while upgrading firmware: %s", err) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn the valve off (closed).""" try: diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 52932cce02b..310f550bcc1 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Configure a local Elexa Guardian device." - }, - "zeroconf_confirm": { - "description": "Do you want to set up this Guardian device?" } } }