diff --git a/.coveragerc b/.coveragerc index b8c7f949c01..58ed78b6dca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -473,6 +473,7 @@ omit = homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py + homeassistant/components/guardian/valve.py homeassistant/components/habitica/__init__.py homeassistant/components/habitica/sensor.py homeassistant/components/harman_kardon_avr/media_player.py diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 117510a8c1a..90504f3213e 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -76,7 +76,13 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( }, ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.SWITCH, + Platform.VALVE, +] @dataclass diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 59630e87932..c426f4f8081 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -44,6 +44,11 @@ "valve_controller": { "name": "Valve controller" } + }, + "valve": { + "valve_controller": { + "name": "Valve controller" + } } }, "services": { @@ -52,7 +57,7 @@ "description": "Adds a new paired sensor to the valve controller.", "fields": { "device_id": { - "name": "[%key:component::guardian::entity::switch::valve_controller::name%]", + "name": "[%key:component::guardian::entity::valve::valve_controller::name%]", "description": "The valve controller to add the sensor to." }, "uid": { @@ -66,7 +71,7 @@ "description": "Removes a paired sensor from the valve controller.", "fields": { "device_id": { - "name": "[%key:component::guardian::entity::switch::valve_controller::name%]", + "name": "[%key:component::guardian::entity::valve::valve_controller::name%]", "description": "The valve controller to remove the sensor from." }, "uid": { @@ -80,7 +85,7 @@ "description": "Upgrades the device firmware.", "fields": { "device_id": { - "name": "[%key:component::guardian::entity::switch::valve_controller::name%]", + "name": "[%key:component::guardian::entity::valve::valve_controller::name%]", "description": "The valve controller whose firmware should be upgraded." }, "url": { diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 1ed5239641d..7db0fde8905 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -10,12 +10,13 @@ from aioguardian import Client from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription from .const import API_VALVE_STATUS, API_WIFI_STATUS, DOMAIN from .util import convert_exceptions_to_homeassistant_error +from .valve import GuardianValveState ATTR_AVG_CURRENT = "average_current" ATTR_CONNECTED_CLIENTS = "connected_clients" @@ -27,13 +28,6 @@ ATTR_TRAVEL_COUNT = "travel_count" SWITCH_KIND_ONBOARD_AP = "onboard_ap" SWITCH_KIND_VALVE = "valve" -ON_STATES = { - "start_opening", - "opening", - "finish_opening", - "opened", -} - @dataclass(frozen=True, kw_only=True) class ValveControllerSwitchDescription( @@ -71,6 +65,17 @@ async def _async_open_valve(client: Client) -> None: await client.valve.open() +@callback +def is_open(data: dict[str, Any]) -> bool: + """Return if the valve is opening.""" + return data["state"] in ( + GuardianValveState.FINISH_OPENING, + GuardianValveState.OPEN, + GuardianValveState.OPENING, + GuardianValveState.START_OPENING, + ) + + VALVE_CONTROLLER_DESCRIPTIONS = ( ValveControllerSwitchDescription( key=SWITCH_KIND_ONBOARD_AP, @@ -97,7 +102,7 @@ VALVE_CONTROLLER_DESCRIPTIONS = ( ATTR_INST_CURRENT_DDT: data["instantaneous_current_ddt"], ATTR_TRAVEL_COUNT: data["travel_count"], }, - is_on_fn=lambda data: data["state"] in ON_STATES, + is_on_fn=is_open, off_fn=_async_close_valve, on_fn=_async_open_valve, ), diff --git a/homeassistant/components/guardian/valve.py b/homeassistant/components/guardian/valve.py new file mode 100644 index 00000000000..94f5ddbee6a --- /dev/null +++ b/homeassistant/components/guardian/valve.py @@ -0,0 +1,190 @@ +"""Valves for the Elexa Guardian integration.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine, Mapping +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from aioguardian import Client + +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityDescription, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_VALVE_STATUS, DOMAIN +from .util import convert_exceptions_to_homeassistant_error + +ATTR_AVG_CURRENT = "average_current" +ATTR_CONNECTED_CLIENTS = "connected_clients" +ATTR_INST_CURRENT = "instantaneous_current" +ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" +ATTR_STATION_CONNECTED = "station_connected" +ATTR_TRAVEL_COUNT = "travel_count" + +VALVE_KIND_VALVE = "valve" + + +class GuardianValveState(StrEnum): + """States of a valve.""" + + CLOSED = "closed" + CLOSING = "closing" + FINISH_CLOSING = "finish_closing" + FINISH_OPENING = "finish_opening" + OPEN = "open" + OPENING = "opening" + START_CLOSING = "start_closing" + START_OPENING = "start_opening" + + +@dataclass(frozen=True, kw_only=True) +class ValveControllerValveDescription( + ValveEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller valve.""" + + extra_state_attributes_fn: Callable[[dict[str, Any]], Mapping[str, Any]] + is_closed_fn: Callable[[dict[str, Any]], bool] + is_closing_fn: Callable[[dict[str, Any]], bool] + is_opening_fn: Callable[[dict[str, Any]], bool] + close_coro_fn: Callable[[Client], Coroutine[Any, Any, None]] + halt_coro_fn: Callable[[Client], Coroutine[Any, Any, None]] + open_coro_fn: Callable[[Client], Coroutine[Any, Any, None]] + + +async def async_close_valve(client: Client) -> None: + """Close the valve.""" + async with client: + await client.valve.close() + + +async def async_halt_valve(client: Client) -> None: + """Halt the valve.""" + async with client: + await client.valve.halt() + + +async def async_open_valve(client: Client) -> None: + """Open the valve.""" + async with client: + await client.valve.open() + + +@callback +def is_closing(data: dict[str, Any]) -> bool: + """Return if the valve is closing.""" + return data["state"] in ( + GuardianValveState.CLOSING, + GuardianValveState.FINISH_CLOSING, + GuardianValveState.START_CLOSING, + ) + + +@callback +def is_opening(data: dict[str, Any]) -> bool: + """Return if the valve is opening.""" + return data["state"] in ( + GuardianValveState.OPENING, + GuardianValveState.FINISH_OPENING, + GuardianValveState.START_OPENING, + ) + + +VALVE_CONTROLLER_DESCRIPTIONS = ( + ValveControllerValveDescription( + key=VALVE_KIND_VALVE, + translation_key="valve_controller", + device_class=ValveDeviceClass.WATER, + api_category=API_VALVE_STATUS, + extra_state_attributes_fn=lambda data: { + ATTR_AVG_CURRENT: data["average_current"], + ATTR_INST_CURRENT: data["instantaneous_current"], + ATTR_INST_CURRENT_DDT: data["instantaneous_current_ddt"], + ATTR_TRAVEL_COUNT: data["travel_count"], + }, + is_closed_fn=lambda data: data["state"] == GuardianValveState.CLOSED, + is_closing_fn=is_closing, + is_opening_fn=is_opening, + close_coro_fn=async_close_valve, + halt_coro_fn=async_halt_valve, + open_coro_fn=async_open_valve, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Guardian switches based on a config entry.""" + data: GuardianData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + ValveControllerValve(entry, data, description) + for description in VALVE_CONTROLLER_DESCRIPTIONS + ) + + +class ValveControllerValve(ValveControllerEntity, ValveEntity): + """Define a switch related to a Guardian valve controller.""" + + _attr_supported_features = ( + ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE | ValveEntityFeature.STOP + ) + entity_description: ValveControllerValveDescription + + def __init__( + self, + entry: ConfigEntry, + data: GuardianData, + description: ValveControllerValveDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, data.valve_controller_coordinators, description) + + self._client = data.client + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + return self.entity_description.extra_state_attributes_fn(self.coordinator.data) + + @property + def is_closing(self) -> bool: + """Return if the valve is closing or not.""" + return self.entity_description.is_closing_fn(self.coordinator.data) + + @property + def is_closed(self) -> bool: + """Return if the valve is closed or not.""" + return self.entity_description.is_closed_fn(self.coordinator.data) + + @property + def is_opening(self) -> bool: + """Return if the valve is opening or not.""" + return self.entity_description.is_opening_fn(self.coordinator.data) + + @convert_exceptions_to_homeassistant_error + async def async_close_valve(self) -> None: + """Close the valve.""" + await self.entity_description.close_coro_fn(self._client) + await self.coordinator.async_request_refresh() + + @convert_exceptions_to_homeassistant_error + async def async_open_valve(self) -> None: + """Open the valve.""" + await self.entity_description.open_coro_fn(self._client) + await self.coordinator.async_request_refresh() + + @convert_exceptions_to_homeassistant_error + async def async_stop_valve(self) -> None: + """Stop the valve.""" + await self.entity_description.halt_coro_fn(self._client) + await self.coordinator.async_request_refresh()