Skip to content

Commit

Permalink
Add flow and rain sensor support to Hydrawise (#116303)
Browse files Browse the repository at this point in the history
* Add flow and rain sensor support to Hydrawise

* Address comments

* Cleanup

* Review comments

* Address review comments

* Added tests

* Add icon translations

* Add snapshot tests

* Clean up binary sensor

* Mypy cleanup

* Another mypy error

* Reviewer feedback

* Clear next_cycle sensor when the value is unknown

* Reviewer feedback

* Reviewer feedback

* Remove assert

* Restructure switches, sensors, and binary sensors

* Reviewer feedback

* Reviewer feedback
  • Loading branch information
thomaskistler committed May 7, 2024
1 parent a3248cc commit 14fcf7b
Show file tree
Hide file tree
Showing 14 changed files with 1,299 additions and 141 deletions.
71 changes: 51 additions & 20 deletions homeassistant/components/hydrawise/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
Expand All @@ -15,22 +18,40 @@
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity

BINARY_SENSOR_STATUS = BinarySensorEntityDescription(
key="status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,

@dataclass(frozen=True, kw_only=True)
class HydrawiseBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Hydrawise binary sensor."""

value_fn: Callable[[HydrawiseBinarySensor], bool | None]


CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
HydrawiseBinarySensorEntityDescription(
key="status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success,
),
)

BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="is_watering",
translation_key="watering",
RAIN_SENSOR_BINARY_SENSOR: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
HydrawiseBinarySensorEntityDescription(
key="rain_sensor",
translation_key="rain_sensor",
device_class=BinarySensorDeviceClass.MOISTURE,
value_fn=lambda rain_sensor: rain_sensor.sensor.status.active,
),
)

BINARY_SENSOR_KEYS: list[str] = [
desc.key for desc in (BINARY_SENSOR_STATUS, *BINARY_SENSOR_TYPES)
]
ZONE_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
HydrawiseBinarySensorEntityDescription(
key="is_watering",
translation_key="watering",
device_class=BinarySensorDeviceClass.RUNNING,
value_fn=lambda watering_sensor: watering_sensor.zone.scheduled_runs.current_run
is not None,
),
)


async def async_setup_entry(
Expand All @@ -42,26 +63,36 @@ async def async_setup_entry(
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
entities = []
entities: list[HydrawiseBinarySensor] = []
for controller in coordinator.data.controllers.values():
entities.append(
HydrawiseBinarySensor(coordinator, BINARY_SENSOR_STATUS, controller)
entities.extend(
HydrawiseBinarySensor(coordinator, description, controller)
for description in CONTROLLER_BINARY_SENSORS
)
entities.extend(
HydrawiseBinarySensor(
coordinator,
description,
controller,
sensor_id=sensor.id,
)
for sensor in controller.sensors
for description in RAIN_SENSOR_BINARY_SENSOR
if "rain sensor" in sensor.model.name.lower()
)
entities.extend(
HydrawiseBinarySensor(coordinator, description, controller, zone)
HydrawiseBinarySensor(coordinator, description, controller, zone_id=zone.id)
for zone in controller.zones
for description in BINARY_SENSOR_TYPES
for description in ZONE_BINARY_SENSORS
)
async_add_entities(entities)


class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
"""A sensor implementation for Hydrawise device."""

entity_description: HydrawiseBinarySensorEntityDescription

def _update_attrs(self) -> None:
"""Update state attributes."""
if self.entity_description.key == "status":
self._attr_is_on = self.coordinator.last_update_success
elif self.entity_description.key == "is_watering":
assert self.zone is not None
self._attr_is_on = self.zone.scheduled_runs.current_run is not None
self._attr_is_on = self.entity_description.value_fn(self)
35 changes: 30 additions & 5 deletions homeassistant/components/hydrawise/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from dataclasses import dataclass
from datetime import timedelta

from pydrawise import HydrawiseBase
from pydrawise.schema import Controller, User, Zone
from pydrawise import Hydrawise
from pydrawise.schema import Controller, ControllerWaterUseSummary, Sensor, User, Zone

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import now

from .const import DOMAIN, LOGGER

Expand All @@ -21,15 +22,17 @@ class HydrawiseData:
user: User
controllers: dict[int, Controller]
zones: dict[int, Zone]
sensors: dict[int, Sensor]
daily_water_use: dict[int, ControllerWaterUseSummary]


class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
"""The Hydrawise Data Update Coordinator."""

api: HydrawiseBase
api: Hydrawise

def __init__(
self, hass: HomeAssistant, api: HydrawiseBase, scan_interval: timedelta
self, hass: HomeAssistant, api: Hydrawise, scan_interval: timedelta
) -> None:
"""Initialize HydrawiseDataUpdateCoordinator."""
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=scan_interval)
Expand All @@ -40,8 +43,30 @@ async def _async_update_data(self) -> HydrawiseData:
user = await self.api.get_user()
controllers = {}
zones = {}
sensors = {}
daily_water_use: dict[int, ControllerWaterUseSummary] = {}
for controller in user.controllers:
controllers[controller.id] = controller
for zone in controller.zones:
zones[zone.id] = zone
return HydrawiseData(user=user, controllers=controllers, zones=zones)
for sensor in controller.sensors:
sensors[sensor.id] = sensor
if any(
"flow meter" in sensor.model.name.lower()
for sensor in controller.sensors
):
daily_water_use[controller.id] = await self.api.get_water_use_summary(
controller,
now().replace(hour=0, minute=0, second=0, microsecond=0),
now(),
)
else:
daily_water_use[controller.id] = ControllerWaterUseSummary()

return HydrawiseData(
user=user,
controllers=controllers,
zones=zones,
sensors=sensors,
daily_water_use=daily_water_use,
)
32 changes: 24 additions & 8 deletions homeassistant/components/hydrawise/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from pydrawise.schema import Controller, Zone
from pydrawise.schema import Controller, Sensor, Zone

from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
Expand All @@ -24,24 +24,42 @@ def __init__(
coordinator: HydrawiseDataUpdateCoordinator,
description: EntityDescription,
controller: Controller,
zone: Zone | None = None,
*,
zone_id: int | None = None,
sensor_id: int | None = None,
) -> None:
"""Initialize the Hydrawise entity."""
super().__init__(coordinator=coordinator)
self.entity_description = description
self.controller = controller
self.zone = zone
self._device_id = str(controller.id if zone is None else zone.id)
self.zone_id = zone_id
self.sensor_id = sensor_id
self._device_id = str(zone_id) if zone_id is not None else str(controller.id)
self._attr_unique_id = f"{self._device_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_id)},
name=controller.name if zone is None else zone.name,
name=self.zone.name if zone_id is not None else controller.name,
model="Zone"
if zone_id is not None
else controller.hardware.model.description,
manufacturer=MANUFACTURER,
)
if zone is not None:
if zone_id is not None or sensor_id is not None:
self._attr_device_info["via_device"] = (DOMAIN, str(controller.id))
self._update_attrs()

@property
def zone(self) -> Zone:
"""Return the entity zone."""
assert self.zone_id is not None # needed for mypy
return self.coordinator.data.zones[self.zone_id]

@property
def sensor(self) -> Sensor:
"""Return the entity sensor."""
assert self.sensor_id is not None # needed for mypy
return self.coordinator.data.sensors[self.sensor_id]

def _update_attrs(self) -> None:
"""Update state attributes."""
return # pragma: no cover
Expand All @@ -50,7 +68,5 @@ def _update_attrs(self) -> None:
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the state."""
self.controller = self.coordinator.data.controllers[self.controller.id]
if self.zone:
self.zone = self.coordinator.data.zones[self.zone.id]
self._update_attrs()
super()._handle_coordinator_update()
23 changes: 22 additions & 1 deletion homeassistant/components/hydrawise/icons.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
{
"entity": {
"sensor": {
"daily_active_water_use": {
"default": "mdi:water"
},
"daily_inactive_water_use": {
"default": "mdi:water"
},
"daily_total_water_use": {
"default": "mdi:water"
},
"next_cycle": {
"default": "mdi:clock-outline"
},
"watering_time": {
"default": "mdi:water-pump"
"default": "mdi:timer-outline"
}
},
"binary_sensor": {
"rain_sensor": {
"default": "mdi:weather-sunny",
"state": {
"off": "mdi:weather-sunny",
"on": "mdi:weather-pouring"
}
}
}
}
Expand Down

0 comments on commit 14fcf7b

Please sign in to comment.