diff --git a/.coveragerc b/.coveragerc index bb3140d7969..e11a268217b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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/* diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 8d3f9444f08..fac929e7e99 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -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: diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index b666d9ed150..7e886dbad90 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -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"] diff --git a/homeassistant/components/rainmachine/model.py b/homeassistant/components/rainmachine/model.py new file mode 100644 index 00000000000..cd66c05025b --- /dev/null +++ b/homeassistant/components/rainmachine/model.py @@ -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 diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 269bd6bcd4b..f990dd5c672 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -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"] diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 361a737218d..a4d4bce2383 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -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], ) )