Finish EntityDescription implementation for RainMachine (#55180)

This commit is contained in:
Aaron Bach 2021-08-25 08:36:25 -06:00 committed by GitHub
parent 53851cb1b4
commit 6bc5c1c9af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 158 deletions

View file

@ -845,6 +845,7 @@ omit =
homeassistant/components/raincloud/*
homeassistant/components/rainmachine/__init__.py
homeassistant/components/rainmachine/binary_sensor.py
homeassistant/components/rainmachine/model.py
homeassistant/components/rainmachine/sensor.py
homeassistant/components/rainmachine/switch.py
homeassistant/components/raspihats/*

View file

@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@ -180,7 +181,7 @@ class RainMachineEntity(CoordinatorEntity):
self,
coordinator: DataUpdateCoordinator,
controller: Controller,
entity_type: str,
description: EntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
@ -200,9 +201,9 @@ class RainMachineEntity(CoordinatorEntity):
# The colons are removed from the device MAC simply because that value
# (unnecessarily) makes up the existing unique ID formula and we want to avoid
# a breaking change:
self._attr_unique_id = f"{controller.mac.replace(':', '')}_{entity_type}"
self._attr_unique_id = f"{controller.mac.replace(':', '')}_{description.key}"
self._controller = controller
self._entity_type = entity_type
self.entity_description = description
@callback
def _handle_coordinator_update(self) -> None:

View file

@ -1,13 +1,14 @@
"""This platform provides binary sensors for key RainMachine data."""
from dataclasses import dataclass
from functools import partial
from regenmaschine.controller import Controller
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import RainMachineEntity
from .const import (
@ -18,6 +19,7 @@ from .const import (
DATA_RESTRICTIONS_UNIVERSAL,
DOMAIN,
)
from .model import RainMachineSensorDescriptionMixin
TYPE_FLOW_SENSOR = "flow_sensor"
TYPE_FREEZE = "freeze"
@ -29,47 +31,75 @@ TYPE_RAINDELAY = "raindelay"
TYPE_RAINSENSOR = "rainsensor"
TYPE_WEEKDAY = "weekday"
BINARY_SENSORS = {
TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True, DATA_PROVISION_SETTINGS),
TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True, DATA_RESTRICTIONS_CURRENT),
TYPE_FREEZE_PROTECTION: (
"Freeze Protection",
"mdi:weather-snowy",
True,
DATA_RESTRICTIONS_UNIVERSAL,
@dataclass
class RainMachineBinarySensorDescription(
BinarySensorEntityDescription, RainMachineSensorDescriptionMixin
):
"""Describe a RainMachine binary sensor."""
BINARY_SENSOR_DESCRIPTIONS = (
RainMachineBinarySensorDescription(
key=TYPE_FLOW_SENSOR,
name="Flow Sensor",
icon="mdi:water-pump",
api_category=DATA_PROVISION_SETTINGS,
),
TYPE_HOT_DAYS: (
"Extra Water on Hot Days",
"mdi:thermometer-lines",
True,
DATA_RESTRICTIONS_UNIVERSAL,
RainMachineBinarySensorDescription(
key=TYPE_FREEZE,
name="Freeze Restrictions",
icon="mdi:cancel",
api_category=DATA_RESTRICTIONS_CURRENT,
),
TYPE_HOURLY: (
"Hourly Restrictions",
"mdi:cancel",
False,
DATA_RESTRICTIONS_CURRENT,
RainMachineBinarySensorDescription(
key=TYPE_FREEZE_PROTECTION,
name="Freeze Protection",
icon="mdi:weather-snowy",
api_category=DATA_RESTRICTIONS_UNIVERSAL,
),
TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False, DATA_RESTRICTIONS_CURRENT),
TYPE_RAINDELAY: (
"Rain Delay Restrictions",
"mdi:cancel",
False,
DATA_RESTRICTIONS_CURRENT,
RainMachineBinarySensorDescription(
key=TYPE_HOT_DAYS,
name="Extra Water on Hot Days",
icon="mdi:thermometer-lines",
api_category=DATA_RESTRICTIONS_UNIVERSAL,
),
TYPE_RAINSENSOR: (
"Rain Sensor Restrictions",
"mdi:cancel",
False,
DATA_RESTRICTIONS_CURRENT,
RainMachineBinarySensorDescription(
key=TYPE_HOURLY,
name="Hourly Restrictions",
icon="mdi:cancel",
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
),
TYPE_WEEKDAY: (
"Weekday Restrictions",
"mdi:cancel",
False,
DATA_RESTRICTIONS_CURRENT,
RainMachineBinarySensorDescription(
key=TYPE_MONTH,
name="Month Restrictions",
icon="mdi:cancel",
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
),
}
RainMachineBinarySensorDescription(
key=TYPE_RAINDELAY,
name="Rain Delay Restrictions",
icon="mdi:cancel",
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
),
RainMachineBinarySensorDescription(
key=TYPE_RAINSENSOR,
name="Rain Sensor Restrictions",
icon="mdi:cancel",
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
),
RainMachineBinarySensorDescription(
key=TYPE_WEEKDAY,
name="Weekday Restrictions",
icon="mdi:cancel",
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
),
)
async def async_setup_entry(
@ -101,74 +131,49 @@ async def async_setup_entry(
async_add_entities(
[
async_get_sensor(api_category)(
controller, sensor_type, name, icon, enabled_by_default
)
for (
sensor_type,
(name, icon, enabled_by_default, api_category),
) in BINARY_SENSORS.items()
async_get_sensor(description.api_category)(controller, description)
for description in BINARY_SENSOR_DESCRIPTIONS
]
)
class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity):
"""Define a general RainMachine binary sensor."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
controller: Controller,
sensor_type: str,
name: str,
icon: str,
enabled_by_default: bool,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, controller, sensor_type)
self._attr_entity_registry_enabled_default = enabled_by_default
self._attr_icon = icon
self._attr_name = name
class CurrentRestrictionsBinarySensor(RainMachineBinarySensor):
class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):
"""Define a binary sensor that handles current restrictions data."""
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
if self._entity_type == TYPE_FREEZE:
if self.entity_description.key == TYPE_FREEZE:
self._attr_is_on = self.coordinator.data["freeze"]
elif self._entity_type == TYPE_HOURLY:
elif self.entity_description.key == TYPE_HOURLY:
self._attr_is_on = self.coordinator.data["hourly"]
elif self._entity_type == TYPE_MONTH:
elif self.entity_description.key == TYPE_MONTH:
self._attr_is_on = self.coordinator.data["month"]
elif self._entity_type == TYPE_RAINDELAY:
elif self.entity_description.key == TYPE_RAINDELAY:
self._attr_is_on = self.coordinator.data["rainDelay"]
elif self._entity_type == TYPE_RAINSENSOR:
elif self.entity_description.key == TYPE_RAINSENSOR:
self._attr_is_on = self.coordinator.data["rainSensor"]
elif self._entity_type == TYPE_WEEKDAY:
elif self.entity_description.key == TYPE_WEEKDAY:
self._attr_is_on = self.coordinator.data["weekDay"]
class ProvisionSettingsBinarySensor(RainMachineBinarySensor):
class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity):
"""Define a binary sensor that handles provisioning data."""
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
if self._entity_type == TYPE_FLOW_SENSOR:
if self.entity_description.key == TYPE_FLOW_SENSOR:
self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor")
class UniversalRestrictionsBinarySensor(RainMachineBinarySensor):
class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):
"""Define a binary sensor that handles universal restrictions data."""
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
if self._entity_type == TYPE_FREEZE_PROTECTION:
if self.entity_description.key == TYPE_FREEZE_PROTECTION:
self._attr_is_on = self.coordinator.data["freezeProtectEnabled"]
elif self._entity_type == TYPE_HOT_DAYS:
elif self.entity_description.key == TYPE_HOT_DAYS:
self._attr_is_on = self.coordinator.data["hotDaysExtraWatering"]

View file

@ -0,0 +1,9 @@
"""Define RainMachine data models."""
from dataclasses import dataclass
@dataclass
class RainMachineSensorDescriptionMixin:
"""Define an entity description mixin for binary and regular sensors."""
api_category: str

View file

@ -4,8 +4,6 @@ from __future__ import annotations
from dataclasses import dataclass
from functools import partial
from regenmaschine.controller import Controller
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -15,7 +13,6 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import RainMachineEntity
from .const import (
@ -25,6 +22,7 @@ from .const import (
DATA_RESTRICTIONS_UNIVERSAL,
DOMAIN,
)
from .model import RainMachineSensorDescriptionMixin
TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter"
TYPE_FLOW_SENSOR_CONSUMED_LITERS = "flow_sensor_consumed_liters"
@ -34,21 +32,14 @@ TYPE_FREEZE_TEMP = "freeze_protect_temp"
@dataclass
class RainmachineRequiredKeysMixin:
"""Mixin for required keys."""
api_category: str
@dataclass
class RainmachineSensorEntityDescription(
SensorEntityDescription, RainmachineRequiredKeysMixin
class RainMachineSensorEntityDescription(
SensorEntityDescription, RainMachineSensorDescriptionMixin
):
"""Describes Rainmachine sensor entity."""
"""Describe a RainMachine sensor."""
SENSOR_TYPES: tuple[RainmachineSensorEntityDescription, ...] = (
RainmachineSensorEntityDescription(
SENSOR_DESCRIPTIONS = (
RainMachineSensorEntityDescription(
key=TYPE_FLOW_SENSOR_CLICK_M3,
name="Flow Sensor Clicks",
icon="mdi:water-pump",
@ -56,7 +47,7 @@ SENSOR_TYPES: tuple[RainmachineSensorEntityDescription, ...] = (
entity_registry_enabled_default=False,
api_category=DATA_PROVISION_SETTINGS,
),
RainmachineSensorEntityDescription(
RainMachineSensorEntityDescription(
key=TYPE_FLOW_SENSOR_CONSUMED_LITERS,
name="Flow Sensor Consumed Liters",
icon="mdi:water-pump",
@ -64,7 +55,7 @@ SENSOR_TYPES: tuple[RainmachineSensorEntityDescription, ...] = (
entity_registry_enabled_default=False,
api_category=DATA_PROVISION_SETTINGS,
),
RainmachineSensorEntityDescription(
RainMachineSensorEntityDescription(
key=TYPE_FLOW_SENSOR_START_INDEX,
name="Flow Sensor Start Index",
icon="mdi:water-pump",
@ -72,7 +63,7 @@ SENSOR_TYPES: tuple[RainmachineSensorEntityDescription, ...] = (
entity_registry_enabled_default=False,
api_category=DATA_PROVISION_SETTINGS,
),
RainmachineSensorEntityDescription(
RainMachineSensorEntityDescription(
key=TYPE_FLOW_SENSOR_WATERING_CLICKS,
name="Flow Sensor Clicks",
icon="mdi:water-pump",
@ -80,13 +71,12 @@ SENSOR_TYPES: tuple[RainmachineSensorEntityDescription, ...] = (
entity_registry_enabled_default=False,
api_category=DATA_PROVISION_SETTINGS,
),
RainmachineSensorEntityDescription(
RainMachineSensorEntityDescription(
key=TYPE_FREEZE_TEMP,
name="Freeze Protect Temperature",
icon="mdi:thermometer",
native_unit_of_measurement=TEMP_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
entity_registry_enabled_default=True,
api_category=DATA_RESTRICTIONS_UNIVERSAL,
),
)
@ -116,38 +106,22 @@ async def async_setup_entry(
async_add_entities(
[
async_get_sensor(description.api_category)(controller, description)
for description in SENSOR_TYPES
for description in SENSOR_DESCRIPTIONS
]
)
class RainMachineSensor(RainMachineEntity, SensorEntity):
"""Define a general RainMachine sensor."""
entity_description: RainmachineSensorEntityDescription
def __init__(
self,
coordinator: DataUpdateCoordinator,
controller: Controller,
description: RainmachineSensorEntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator, controller, description.key)
self.entity_description = description
class ProvisionSettingsSensor(RainMachineSensor):
class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
"""Define a sensor that handles provisioning data."""
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
if self._entity_type == TYPE_FLOW_SENSOR_CLICK_M3:
if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3:
self._attr_native_value = self.coordinator.data["system"].get(
"flowSensorClicksPerCubicMeter"
)
elif self._entity_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
clicks = self.coordinator.data["system"].get("flowSensorWateringClicks")
clicks_per_m3 = self.coordinator.data["system"].get(
"flowSensorClicksPerCubicMeter"
@ -157,21 +131,21 @@ class ProvisionSettingsSensor(RainMachineSensor):
self._attr_native_value = (clicks * 1000) / clicks_per_m3
else:
self._attr_native_value = None
elif self._entity_type == TYPE_FLOW_SENSOR_START_INDEX:
elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX:
self._attr_native_value = self.coordinator.data["system"].get(
"flowSensorStartIndex"
)
elif self._entity_type == TYPE_FLOW_SENSOR_WATERING_CLICKS:
elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS:
self._attr_native_value = self.coordinator.data["system"].get(
"flowSensorWateringClicks"
)
class UniversalRestrictionsSensor(RainMachineSensor):
class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity):
"""Define a sensor that handles universal restrictions data."""
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
if self._entity_type == TYPE_FREEZE_TEMP:
if self.entity_description.key == TYPE_FREEZE_TEMP:
self._attr_native_value = self.coordinator.data["freezeProtectTemp"]

View file

@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Coroutine
from dataclasses import dataclass
from datetime import datetime
from typing import Any
@ -9,7 +10,7 @@ from regenmaschine.controller import Controller
from regenmaschine.errors import RequestError
import voluptuous as vol
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ID
from homeassistant.core import HomeAssistant, callback
@ -115,6 +116,20 @@ SWITCH_TYPE_PROGRAM = "program"
SWITCH_TYPE_ZONE = "zone"
@dataclass
class RainMachineSwitchDescriptionMixin:
"""Define an entity description mixin for switches."""
uid: int
@dataclass
class RainMachineSwitchDescription(
SwitchEntityDescription, RainMachineSwitchDescriptionMixin
):
"""Describe a RainMachine switch."""
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@ -166,18 +181,34 @@ async def async_setup_entry(
]
zones_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][DATA_ZONES]
entities: list[RainMachineProgram | RainMachineZone] = []
for uid, program in programs_coordinator.data.items():
entities.append(
RainMachineProgram(
programs_coordinator, controller, uid, program["name"], entry
entities: list[RainMachineProgram | RainMachineZone] = [
RainMachineProgram(
programs_coordinator,
controller,
entry,
RainMachineSwitchDescription(
key=f"RainMachineProgram_{uid}",
name=program["name"],
uid=uid,
),
)
for uid, program in programs_coordinator.data.items()
]
entities.extend(
[
RainMachineZone(
zones_coordinator,
controller,
entry,
RainMachineSwitchDescription(
key=f"RainMachineZone_{uid}",
name=zone["name"],
uid=uid,
),
)
)
for uid, zone in zones_coordinator.data.items():
entities.append(
RainMachineZone(zones_coordinator, controller, uid, zone["name"], entry)
)
for uid, zone in zones_coordinator.data.items()
]
)
async_add_entities(entities)
@ -186,35 +217,28 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
"""A class to represent a generic RainMachine switch."""
_attr_icon = DEFAULT_ICON
entity_description: RainMachineSwitchDescription
def __init__(
self,
coordinator: DataUpdateCoordinator,
controller: Controller,
uid: int,
name: str,
entry: ConfigEntry,
description: RainMachineSwitchDescription,
) -> None:
"""Initialize a generic RainMachine switch."""
super().__init__(coordinator, controller, type(self).__name__)
super().__init__(coordinator, controller, description)
self._attr_is_on = False
self._attr_name = name
self._data = coordinator.data[uid]
self._data = coordinator.data[self.entity_description.uid]
self._entry = entry
self._is_active = True
self._uid = uid
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._is_active
@property
def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity."""
return f"{super().unique_id}_{self._uid}"
async def _async_run_switch_coroutine(self, api_coro: Coroutine) -> None:
"""Run a coroutine to toggle the switch."""
try:
@ -222,7 +246,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
except RequestError as err:
LOGGER.error(
'Error while toggling %s "%s": %s',
self._entity_type,
self.entity_description.key,
self.unique_id,
err,
)
@ -231,7 +255,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
if resp["statusCode"] != 0:
LOGGER.error(
'Error while toggling %s "%s": %s',
self._entity_type,
self.entity_description.key,
self.unique_id,
resp["message"],
)
@ -301,7 +325,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
@callback
def update_from_latest_data(self) -> None:
"""Update the state."""
self._data = self.coordinator.data[self._uid]
self._data = self.coordinator.data[self.entity_description.uid]
self._is_active = self._data["active"]
@ -316,13 +340,13 @@ class RainMachineProgram(RainMachineSwitch):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the program off."""
await self._async_run_switch_coroutine(
self._controller.programs.stop(self._uid)
self._controller.programs.stop(self.entity_description.uid)
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the program on."""
await self._async_run_switch_coroutine(
self._controller.programs.start(self._uid)
self._controller.programs.start(self.entity_description.uid)
)
@callback
@ -341,10 +365,14 @@ class RainMachineProgram(RainMachineSwitch):
self._attr_extra_state_attributes.update(
{
ATTR_ID: self._uid,
ATTR_ID: self.entity_description.uid,
ATTR_NEXT_RUN: next_run,
ATTR_SOAK: self.coordinator.data[self._uid].get("soak"),
ATTR_STATUS: RUN_STATUS_MAP[self.coordinator.data[self._uid]["status"]],
ATTR_SOAK: self.coordinator.data[self.entity_description.uid].get(
"soak"
),
ATTR_STATUS: RUN_STATUS_MAP[
self.coordinator.data[self.entity_description.uid]["status"]
],
ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
}
)
@ -355,13 +383,15 @@ class RainMachineZone(RainMachineSwitch):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self._async_run_switch_coroutine(self._controller.zones.stop(self._uid))
await self._async_run_switch_coroutine(
self._controller.zones.stop(self.entity_description.uid)
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self._async_run_switch_coroutine(
self._controller.zones.start(
self._uid,
self.entity_description.uid,
self._entry.options[CONF_ZONE_RUN_TIME],
)
)