Simplify Guardian entity inheritance hierarchy (#75274)

This commit is contained in:
Aaron Bach 2022-07-18 09:18:07 -06:00 committed by GitHub
parent 6f5e4ca503
commit b3ef6f4d04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 225 additions and 248 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import cast from typing import cast
from aioguardian import Client from aioguardian import Client
@ -24,10 +25,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import CoordinatorEntity
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import ( from .const import (
API_SENSOR_PAIR_DUMP, API_SENSOR_PAIR_DUMP,
@ -132,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# so we use a lock to ensure that only one API request is reaching it at a time: # so we use a lock to ensure that only one API request is reaching it at a time:
api_lock = asyncio.Lock() api_lock = asyncio.Lock()
# Set up DataUpdateCoordinators for the valve controller: # Set up GuardianDataUpdateCoordinators for the valve controller:
coordinators: dict[str, GuardianDataUpdateCoordinator] = {} coordinators: dict[str, GuardianDataUpdateCoordinator] = {}
init_valve_controller_tasks = [] init_valve_controller_tasks = []
for api, api_coro in ( for api, api_coro in (
@ -418,17 +416,18 @@ class PairedSensorManager:
dev_reg.async_remove_device(device.id) dev_reg.async_remove_device(device.id)
class GuardianEntity(CoordinatorEntity): class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]):
"""Define a base Guardian entity.""" """Define a base Guardian entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( # pylint: disable=super-init-not-called def __init__(
self, entry: ConfigEntry, description: EntityDescription self, coordinator: GuardianDataUpdateCoordinator, description: EntityDescription
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator)
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
self._entry = entry
self.entity_description = description self.entity_description = description
@callback @callback
@ -438,6 +437,17 @@ class GuardianEntity(CoordinatorEntity):
This should be extended by Guardian platforms. This should be extended by Guardian platforms.
""" """
@callback
def _handle_coordinator_update(self) -> None:
"""Respond to a DataUpdateCoordinator update."""
self._async_update_from_latest_data()
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._async_update_from_latest_data()
class PairedSensorEntity(GuardianEntity): class PairedSensorEntity(GuardianEntity):
"""Define a Guardian paired sensor entity.""" """Define a Guardian paired sensor entity."""
@ -445,11 +455,11 @@ class PairedSensorEntity(GuardianEntity):
def __init__( def __init__(
self, self,
entry: ConfigEntry, entry: ConfigEntry,
coordinator: DataUpdateCoordinator, coordinator: GuardianDataUpdateCoordinator,
description: EntityDescription, description: EntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(entry, description) super().__init__(coordinator, description)
paired_sensor_uid = coordinator.data["uid"] paired_sensor_uid = coordinator.data["uid"]
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
@ -460,11 +470,20 @@ class PairedSensorEntity(GuardianEntity):
via_device=(DOMAIN, entry.data[CONF_UID]), via_device=(DOMAIN, entry.data[CONF_UID]),
) )
self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" self._attr_unique_id = f"{paired_sensor_uid}_{description.key}"
self.coordinator = coordinator
async def async_added_to_hass(self) -> None:
"""Perform tasks when the entity is added.""" @dataclass
self._async_update_from_latest_data() class ValveControllerEntityDescriptionMixin:
"""Define an entity description mixin for valve controller entities."""
api_category: str
@dataclass
class ValveControllerEntityDescription(
EntityDescription, ValveControllerEntityDescriptionMixin
):
"""Describe a Guardian valve controller entity."""
class ValveControllerEntity(GuardianEntity): class ValveControllerEntity(GuardianEntity):
@ -473,64 +492,18 @@ class ValveControllerEntity(GuardianEntity):
def __init__( def __init__(
self, self,
entry: ConfigEntry, entry: ConfigEntry,
coordinators: dict[str, DataUpdateCoordinator], coordinators: dict[str, GuardianDataUpdateCoordinator],
description: EntityDescription, description: ValveControllerEntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(entry, description) super().__init__(coordinators[description.api_category], description)
self._diagnostics_coordinator = coordinators[API_SYSTEM_DIAGNOSTICS]
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.data[CONF_UID])}, identifiers={(DOMAIN, entry.data[CONF_UID])},
manufacturer="Elexa", manufacturer="Elexa",
model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], model=self._diagnostics_coordinator.data["firmware"],
name=f"Guardian valve controller {entry.data[CONF_UID]}", name=f"Guardian valve controller {entry.data[CONF_UID]}",
) )
self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}"
self.coordinators = coordinators
@property
def available(self) -> bool:
"""Return if entity is available."""
return any(
coordinator.last_update_success
for coordinator in self.coordinators.values()
)
async def _async_continue_entity_setup(self) -> None:
"""Perform additional, internal tasks when the entity is about to be added.
This should be extended by Guardian platforms.
"""
@callback
def async_add_coordinator_update_listener(self, api: str) -> None:
"""Add a listener to a DataUpdateCoordinator based on the API referenced."""
@callback
def update() -> None:
"""Update the entity's state."""
self._async_update_from_latest_data()
self.async_write_ha_state()
self.async_on_remove(self.coordinators[api].async_add_listener(update))
async def async_added_to_hass(self) -> None:
"""Perform tasks when the entity is added."""
await self._async_continue_entity_setup()
self.async_add_coordinator_update_listener(API_SYSTEM_DIAGNOSTICS)
self._async_update_from_latest_data()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
# Ignore manual update requests if the entity is disabled
if not self.enabled:
return
refresh_tasks = [
coordinator.async_request_refresh()
for coordinator in self.coordinators.values()
]
await asyncio.gather(*refresh_tasks)

View File

@ -1,6 +1,8 @@
"""Binary sensors for the Elexa Guardian integration.""" """Binary sensors for the Elexa Guardian integration."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
@ -11,9 +13,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import PairedSensorEntity, ValveControllerEntity from . import (
PairedSensorEntity,
ValveControllerEntity,
ValveControllerEntityDescription,
)
from .const import ( from .const import (
API_SYSTEM_ONBOARD_SENSOR_STATUS, API_SYSTEM_ONBOARD_SENSOR_STATUS,
API_WIFI_STATUS, API_WIFI_STATUS,
@ -23,6 +28,7 @@ from .const import (
DOMAIN, DOMAIN,
SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED,
) )
from .util import GuardianDataUpdateCoordinator
ATTR_CONNECTED_CLIENTS = "connected_clients" ATTR_CONNECTED_CLIENTS = "connected_clients"
@ -30,31 +36,42 @@ SENSOR_KIND_AP_INFO = "ap_enabled"
SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSOR_KIND_LEAK_DETECTED = "leak_detected"
SENSOR_KIND_MOVED = "moved" SENSOR_KIND_MOVED = "moved"
SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription(
key=SENSOR_KIND_AP_INFO, @dataclass
name="Onboard AP enabled", class ValveControllerBinarySensorDescription(
device_class=BinarySensorDeviceClass.CONNECTIVITY, BinarySensorEntityDescription, ValveControllerEntityDescription
entity_category=EntityCategory.DIAGNOSTIC, ):
) """Describe a Guardian valve controller binary sensor."""
SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription(
PAIRED_SENSOR_DESCRIPTIONS = (
BinarySensorEntityDescription(
key=SENSOR_KIND_LEAK_DETECTED, key=SENSOR_KIND_LEAK_DETECTED,
name="Leak detected", name="Leak detected",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
) ),
SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( BinarySensorEntityDescription(
key=SENSOR_KIND_MOVED, key=SENSOR_KIND_MOVED,
name="Recently moved", name="Recently moved",
device_class=BinarySensorDeviceClass.MOVING, device_class=BinarySensorDeviceClass.MOVING,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
),
) )
PAIRED_SENSOR_DESCRIPTIONS = (
SENSOR_DESCRIPTION_LEAK_DETECTED,
SENSOR_DESCRIPTION_MOVED,
)
VALVE_CONTROLLER_DESCRIPTIONS = ( VALVE_CONTROLLER_DESCRIPTIONS = (
SENSOR_DESCRIPTION_AP_ENABLED, ValveControllerBinarySensorDescription(
SENSOR_DESCRIPTION_LEAK_DETECTED, key=SENSOR_KIND_LEAK_DETECTED,
name="Leak detected",
device_class=BinarySensorDeviceClass.MOISTURE,
api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS,
),
ValveControllerBinarySensorDescription(
key=SENSOR_KIND_AP_INFO,
name="Onboard AP enabled",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
api_category=API_WIFI_STATUS,
),
) )
@ -62,19 +79,18 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Guardian switches based on a config entry.""" """Set up Guardian switches based on a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR]
valve_controller_coordinators = entry_data[DATA_COORDINATOR]
@callback @callback
def add_new_paired_sensor(uid: str) -> None: def add_new_paired_sensor(uid: str) -> None:
"""Add a new paired sensor.""" """Add a new paired sensor."""
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][
uid
]
async_add_entities( async_add_entities(
[ PairedSensorBinarySensor(
PairedSensorBinarySensor(entry, coordinator, description) entry, paired_sensor_coordinators[uid], description
)
for description in PAIRED_SENSOR_DESCRIPTIONS for description in PAIRED_SENSOR_DESCRIPTIONS
]
) )
# Handle adding paired sensors after HASS startup: # Handle adding paired sensors after HASS startup:
@ -88,9 +104,7 @@ async def async_setup_entry(
# Add all valve controller-specific binary sensors: # Add all valve controller-specific binary sensors:
sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [
ValveControllerBinarySensor( ValveControllerBinarySensor(entry, valve_controller_coordinators, description)
entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description
)
for description in VALVE_CONTROLLER_DESCRIPTIONS for description in VALVE_CONTROLLER_DESCRIPTIONS
] ]
@ -98,9 +112,7 @@ async def async_setup_entry(
sensors.extend( sensors.extend(
[ [
PairedSensorBinarySensor(entry, coordinator, description) PairedSensorBinarySensor(entry, coordinator, description)
for coordinator in hass.data[DOMAIN][entry.entry_id][ for coordinator in paired_sensor_coordinators.values()
DATA_COORDINATOR_PAIRED_SENSOR
].values()
for description in PAIRED_SENSOR_DESCRIPTIONS for description in PAIRED_SENSOR_DESCRIPTIONS
] ]
) )
@ -111,10 +123,12 @@ async def async_setup_entry(
class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity):
"""Define a binary sensor related to a Guardian valve controller.""" """Define a binary sensor related to a Guardian valve controller."""
entity_description: BinarySensorEntityDescription
def __init__( def __init__(
self, self,
entry: ConfigEntry, entry: ConfigEntry,
coordinator: DataUpdateCoordinator, coordinator: GuardianDataUpdateCoordinator,
description: BinarySensorEntityDescription, description: BinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
@ -134,45 +148,26 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity):
class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity):
"""Define a binary sensor related to a Guardian valve controller.""" """Define a binary sensor related to a Guardian valve controller."""
entity_description: ValveControllerBinarySensorDescription
def __init__( def __init__(
self, self,
entry: ConfigEntry, entry: ConfigEntry,
coordinators: dict[str, DataUpdateCoordinator], coordinators: dict[str, GuardianDataUpdateCoordinator],
description: BinarySensorEntityDescription, description: ValveControllerBinarySensorDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(entry, coordinators, description) super().__init__(entry, coordinators, description)
self._attr_is_on = True self._attr_is_on = True
async def _async_continue_entity_setup(self) -> None:
"""Add an API listener."""
if self.entity_description.key == SENSOR_KIND_AP_INFO:
self.async_add_coordinator_update_listener(API_WIFI_STATUS)
elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED:
self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS)
@callback @callback
def _async_update_from_latest_data(self) -> None: def _async_update_from_latest_data(self) -> None:
"""Update the entity.""" """Update the entity."""
if self.entity_description.key == SENSOR_KIND_AP_INFO: if self.entity_description.key == SENSOR_KIND_AP_INFO:
self._attr_available = self.coordinators[ self._attr_is_on = self.coordinator.data["station_connected"]
API_WIFI_STATUS self._attr_extra_state_attributes[
].last_update_success ATTR_CONNECTED_CLIENTS
self._attr_is_on = self.coordinators[API_WIFI_STATUS].data[ ] = self.coordinator.data.get("ap_clients")
"station_connected"
]
self._attr_extra_state_attributes.update(
{
ATTR_CONNECTED_CLIENTS: self.coordinators[API_WIFI_STATUS].data.get(
"ap_clients"
)
}
)
elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED:
self._attr_available = self.coordinators[ self._attr_is_on = self.coordinator.data["wet"]
API_SYSTEM_ONBOARD_SENSOR_STATUS
].last_update_success
self._attr_is_on = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[
"wet"
]

View File

@ -17,24 +17,26 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import ValveControllerEntity from . import ValveControllerEntity, ValveControllerEntityDescription
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN from .const import API_SYSTEM_DIAGNOSTICS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN
from .util import GuardianDataUpdateCoordinator
@dataclass @dataclass
class GuardianButtonDescriptionMixin: class GuardianButtonEntityDescriptionMixin:
"""Define an entity description mixin for Guardian buttons.""" """Define an mixin for button entities."""
push_action: Callable[[Client], Awaitable] push_action: Callable[[Client], Awaitable]
@dataclass @dataclass
class GuardianButtonDescription( class ValveControllerButtonDescription(
ButtonEntityDescription, GuardianButtonDescriptionMixin ButtonEntityDescription,
ValveControllerEntityDescription,
GuardianButtonEntityDescriptionMixin,
): ):
"""Describe a Guardian button description.""" """Describe a Guardian valve controller button."""
BUTTON_KIND_REBOOT = "reboot" BUTTON_KIND_REBOOT = "reboot"
@ -52,15 +54,21 @@ async def _async_valve_reset(client: Client) -> None:
BUTTON_DESCRIPTIONS = ( BUTTON_DESCRIPTIONS = (
GuardianButtonDescription( ValveControllerButtonDescription(
key=BUTTON_KIND_REBOOT, key=BUTTON_KIND_REBOOT,
name="Reboot", name="Reboot",
push_action=_async_reboot, push_action=_async_reboot,
# Buttons don't actually need a coordinator; we give them one so they can
# properly inherit from GuardianEntity:
api_category=API_SYSTEM_DIAGNOSTICS,
), ),
GuardianButtonDescription( ValveControllerButtonDescription(
key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS,
name="Reset valve diagnostics", name="Reset valve diagnostics",
push_action=_async_valve_reset, push_action=_async_valve_reset,
# Buttons don't actually need a coordinator; we give them one so they can
# properly inherit from GuardianEntity:
api_category=API_SYSTEM_DIAGNOSTICS,
), ),
) )
@ -69,16 +77,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Guardian buttons based on a config entry.""" """Set up Guardian buttons based on a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
client = entry_data[DATA_CLIENT]
valve_controller_coordinators = entry_data[DATA_COORDINATOR]
async_add_entities( async_add_entities(
[ GuardianButton(entry, valve_controller_coordinators, description, client)
GuardianButton(
entry,
hass.data[DOMAIN][entry.entry_id][DATA_CLIENT],
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
description,
)
for description in BUTTON_DESCRIPTIONS for description in BUTTON_DESCRIPTIONS
]
) )
@ -88,14 +93,14 @@ class GuardianButton(ValveControllerEntity, ButtonEntity):
_attr_device_class = ButtonDeviceClass.RESTART _attr_device_class = ButtonDeviceClass.RESTART
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
entity_description: GuardianButtonDescription entity_description: ValveControllerButtonDescription
def __init__( def __init__(
self, self,
entry: ConfigEntry, entry: ConfigEntry,
coordinators: dict[str, GuardianDataUpdateCoordinator],
description: ValveControllerButtonDescription,
client: Client, client: Client,
coordinators: dict[str, DataUpdateCoordinator],
description: GuardianButtonDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(entry, coordinators, description) super().__init__(entry, coordinators, description)

View File

@ -1,6 +1,8 @@
"""Sensors for the Elexa Guardian integration.""" """Sensors for the Elexa Guardian integration."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@ -14,7 +16,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import PairedSensorEntity, ValveControllerEntity from . import (
PairedSensorEntity,
ValveControllerEntity,
ValveControllerEntityDescription,
)
from .const import ( from .const import (
API_SYSTEM_DIAGNOSTICS, API_SYSTEM_DIAGNOSTICS,
API_SYSTEM_ONBOARD_SENSOR_STATUS, API_SYSTEM_ONBOARD_SENSOR_STATUS,
@ -29,35 +35,47 @@ SENSOR_KIND_BATTERY = "battery"
SENSOR_KIND_TEMPERATURE = "temperature" SENSOR_KIND_TEMPERATURE = "temperature"
SENSOR_KIND_UPTIME = "uptime" SENSOR_KIND_UPTIME = "uptime"
SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription(
@dataclass
class ValveControllerSensorDescription(
SensorEntityDescription, ValveControllerEntityDescription
):
"""Describe a Guardian valve controller sensor."""
PAIRED_SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=SENSOR_KIND_BATTERY, key=SENSOR_KIND_BATTERY,
name="Battery", name="Battery",
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
) ),
SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( SensorEntityDescription(
key=SENSOR_KIND_TEMPERATURE, key=SENSOR_KIND_TEMPERATURE,
name="Temperature", name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_FAHRENHEIT, native_unit_of_measurement=TEMP_FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
),
) )
SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( VALVE_CONTROLLER_DESCRIPTIONS = (
ValveControllerSensorDescription(
key=SENSOR_KIND_TEMPERATURE,
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT,
api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS,
),
ValveControllerSensorDescription(
key=SENSOR_KIND_UPTIME, key=SENSOR_KIND_UPTIME,
name="Uptime", name="Uptime",
icon="mdi:timer", icon="mdi:timer",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=TIME_MINUTES, native_unit_of_measurement=TIME_MINUTES,
) api_category=API_SYSTEM_DIAGNOSTICS,
),
PAIRED_SENSOR_DESCRIPTIONS = (
SENSOR_DESCRIPTION_BATTERY,
SENSOR_DESCRIPTION_TEMPERATURE,
)
VALVE_CONTROLLER_DESCRIPTIONS = (
SENSOR_DESCRIPTION_TEMPERATURE,
SENSOR_DESCRIPTION_UPTIME,
) )
@ -65,19 +83,16 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Guardian switches based on a config entry.""" """Set up Guardian switches based on a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR]
valve_controller_coordinators = entry_data[DATA_COORDINATOR]
@callback @callback
def add_new_paired_sensor(uid: str) -> None: def add_new_paired_sensor(uid: str) -> None:
"""Add a new paired sensor.""" """Add a new paired sensor."""
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][
uid
]
async_add_entities( async_add_entities(
[ PairedSensorSensor(entry, paired_sensor_coordinators[uid], description)
PairedSensorSensor(entry, coordinator, description)
for description in PAIRED_SENSOR_DESCRIPTIONS for description in PAIRED_SENSOR_DESCRIPTIONS
]
) )
# Handle adding paired sensors after HASS startup: # Handle adding paired sensors after HASS startup:
@ -91,9 +106,7 @@ async def async_setup_entry(
# Add all valve controller-specific binary sensors: # Add all valve controller-specific binary sensors:
sensors: list[PairedSensorSensor | ValveControllerSensor] = [ sensors: list[PairedSensorSensor | ValveControllerSensor] = [
ValveControllerSensor( ValveControllerSensor(entry, valve_controller_coordinators, description)
entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description
)
for description in VALVE_CONTROLLER_DESCRIPTIONS for description in VALVE_CONTROLLER_DESCRIPTIONS
] ]
@ -101,9 +114,7 @@ async def async_setup_entry(
sensors.extend( sensors.extend(
[ [
PairedSensorSensor(entry, coordinator, description) PairedSensorSensor(entry, coordinator, description)
for coordinator in hass.data[DOMAIN][entry.entry_id][ for coordinator in paired_sensor_coordinators.values()
DATA_COORDINATOR_PAIRED_SENSOR
].values()
for description in PAIRED_SENSOR_DESCRIPTIONS for description in PAIRED_SENSOR_DESCRIPTIONS
] ]
) )
@ -114,6 +125,8 @@ async def async_setup_entry(
class PairedSensorSensor(PairedSensorEntity, SensorEntity): class PairedSensorSensor(PairedSensorEntity, SensorEntity):
"""Define a binary sensor related to a Guardian valve controller.""" """Define a binary sensor related to a Guardian valve controller."""
entity_description: SensorEntityDescription
@callback @callback
def _async_update_from_latest_data(self) -> None: def _async_update_from_latest_data(self) -> None:
"""Update the entity.""" """Update the entity."""
@ -126,25 +139,12 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity):
class ValveControllerSensor(ValveControllerEntity, SensorEntity): class ValveControllerSensor(ValveControllerEntity, SensorEntity):
"""Define a generic Guardian sensor.""" """Define a generic Guardian sensor."""
async def _async_continue_entity_setup(self) -> None: entity_description: ValveControllerSensorDescription
"""Register API interest (and related tasks) when the entity is added."""
if self.entity_description.key == SENSOR_KIND_TEMPERATURE:
self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS)
@callback @callback
def _async_update_from_latest_data(self) -> None: def _async_update_from_latest_data(self) -> None:
"""Update the entity.""" """Update the entity."""
if self.entity_description.key == SENSOR_KIND_TEMPERATURE: if self.entity_description.key == SENSOR_KIND_TEMPERATURE:
self._attr_available = self.coordinators[ self._attr_native_value = self.coordinator.data["temperature"]
API_SYSTEM_ONBOARD_SENSOR_STATUS
].last_update_success
self._attr_native_value = self.coordinators[
API_SYSTEM_ONBOARD_SENSOR_STATUS
].data["temperature"]
elif self.entity_description.key == SENSOR_KIND_UPTIME: elif self.entity_description.key == SENSOR_KIND_UPTIME:
self._attr_available = self.coordinators[ self._attr_native_value = self.coordinator.data["uptime"]
API_SYSTEM_DIAGNOSTICS
].last_update_success
self._attr_native_value = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[
"uptime"
]

View File

@ -1,6 +1,7 @@
"""Switches for the Elexa Guardian integration.""" """Switches for the Elexa Guardian integration."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any from typing import Any
from aioguardian import Client from aioguardian import Client
@ -11,10 +12,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import ValveControllerEntity from . import ValveControllerEntity, ValveControllerEntityDescription
from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN
from .util import GuardianDataUpdateCoordinator
ATTR_AVG_CURRENT = "average_current" ATTR_AVG_CURRENT = "average_current"
ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT = "instantaneous_current"
@ -23,10 +24,21 @@ ATTR_TRAVEL_COUNT = "travel_count"
SWITCH_KIND_VALVE = "valve" SWITCH_KIND_VALVE = "valve"
SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription(
@dataclass
class ValveControllerSwitchDescription(
SwitchEntityDescription, ValveControllerEntityDescription
):
"""Describe a Guardian valve controller switch."""
VALVE_CONTROLLER_DESCRIPTIONS = (
ValveControllerSwitchDescription(
key=SWITCH_KIND_VALVE, key=SWITCH_KIND_VALVE,
name="Valve controller", name="Valve controller",
icon="mdi:water", icon="mdi:water",
api_category=API_VALVE_STATUS,
),
) )
@ -34,61 +46,53 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Guardian switches based on a config entry.""" """Set up Guardian switches based on a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
client = entry_data[DATA_CLIENT]
valve_controller_coordinators = entry_data[DATA_COORDINATOR]
async_add_entities( async_add_entities(
[ ValveControllerSwitch(entry, valve_controller_coordinators, description, client)
ValveControllerSwitch( for description in VALVE_CONTROLLER_DESCRIPTIONS
entry,
hass.data[DOMAIN][entry.entry_id][DATA_CLIENT],
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
)
]
) )
class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
"""Define a switch to open/close the Guardian valve.""" """Define a switch to open/close the Guardian valve."""
def __init__( entity_description: ValveControllerSwitchDescription
self,
entry: ConfigEntry,
client: Client,
coordinators: dict[str, DataUpdateCoordinator],
) -> None:
"""Initialize."""
super().__init__(entry, coordinators, SWITCH_DESCRIPTION_VALVE)
self._attr_is_on = True ON_STATES = {
self._client = client
async def _async_continue_entity_setup(self) -> None:
"""Register API interest (and related tasks) when the entity is added."""
self.async_add_coordinator_update_listener(API_VALVE_STATUS)
@callback
def _async_update_from_latest_data(self) -> None:
"""Update the entity."""
self._attr_available = self.coordinators[API_VALVE_STATUS].last_update_success
self._attr_is_on = self.coordinators[API_VALVE_STATUS].data["state"] in (
"start_opening", "start_opening",
"opening", "opening",
"finish_opening", "finish_opening",
"opened", "opened",
) }
def __init__(
self,
entry: ConfigEntry,
coordinators: dict[str, GuardianDataUpdateCoordinator],
description: ValveControllerSwitchDescription,
client: Client,
) -> None:
"""Initialize."""
super().__init__(entry, coordinators, description)
self._attr_is_on = True
self._client = client
@callback
def _async_update_from_latest_data(self) -> None:
"""Update the entity."""
self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES
self._attr_extra_state_attributes.update( self._attr_extra_state_attributes.update(
{ {
ATTR_AVG_CURRENT: self.coordinators[API_VALVE_STATUS].data[ ATTR_AVG_CURRENT: self.coordinator.data["average_current"],
"average_current" ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"],
], ATTR_INST_CURRENT_DDT: self.coordinator.data[
ATTR_INST_CURRENT: self.coordinators[API_VALVE_STATUS].data[
"instantaneous_current"
],
ATTR_INST_CURRENT_DDT: self.coordinators[API_VALVE_STATUS].data[
"instantaneous_current_ddt" "instantaneous_current_ddt"
], ],
ATTR_TRAVEL_COUNT: self.coordinators[API_VALVE_STATUS].data[ ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"],
"travel_count"
],
} }
) )