Add active watering time sensor to Hydrawise (#120177)

This commit is contained in:
Thomas Kistler 2024-06-26 00:24:48 -07:00 committed by GitHub
parent 30e0bcb324
commit bff9d12cc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 263 additions and 51 deletions

View File

@ -23,7 +23,7 @@ class HydrawiseData:
controllers: dict[int, Controller]
zones: dict[int, Zone]
sensors: dict[int, Sensor]
daily_water_use: dict[int, ControllerWaterUseSummary]
daily_water_summary: dict[int, ControllerWaterUseSummary]
class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
@ -47,7 +47,7 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
controllers = {}
zones = {}
sensors = {}
daily_water_use: dict[int, ControllerWaterUseSummary] = {}
daily_water_summary: dict[int, ControllerWaterUseSummary] = {}
for controller in user.controllers:
controllers[controller.id] = controller
controller.zones = await self.api.get_zones(controller)
@ -55,22 +55,16 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
zones[zone.id] = zone
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()
daily_water_summary[controller.id] = await self.api.get_water_use_summary(
controller,
now().replace(hour=0, minute=0, second=0, microsecond=0),
now(),
)
return HydrawiseData(
user=user,
controllers=controllers,
zones=zones,
sensors=sensors,
daily_water_use=daily_water_use,
daily_water_summary=daily_water_summary,
)

View File

@ -4,6 +4,9 @@
"daily_active_water_use": {
"default": "mdi:water"
},
"daily_active_water_time": {
"default": "mdi:timelapse"
},
"daily_inactive_water_use": {
"default": "mdi:water"
},

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timedelta
from typing import Any
from homeassistant.components.sensor import (
@ -44,28 +44,65 @@ def _get_zone_next_cycle(sensor: HydrawiseSensor) -> datetime | None:
def _get_zone_daily_active_water_use(sensor: HydrawiseSensor) -> float:
"""Get active water use for the zone."""
daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id]
daily_water_summary = sensor.coordinator.data.daily_water_summary[
sensor.controller.id
]
return float(daily_water_summary.active_use_by_zone_id.get(sensor.zone.id, 0.0))
def _get_zone_daily_active_water_time(sensor: HydrawiseSensor) -> float | None:
"""Get active water time for the zone."""
daily_water_summary = sensor.coordinator.data.daily_water_summary[
sensor.controller.id
]
return daily_water_summary.active_time_by_zone_id.get(
sensor.zone.id, timedelta()
).total_seconds()
def _get_controller_daily_active_water_use(sensor: HydrawiseSensor) -> float | None:
"""Get active water use for the controller."""
daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id]
daily_water_summary = sensor.coordinator.data.daily_water_summary[
sensor.controller.id
]
return daily_water_summary.total_active_use
def _get_controller_daily_inactive_water_use(sensor: HydrawiseSensor) -> float | None:
"""Get inactive water use for the controller."""
daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id]
daily_water_summary = sensor.coordinator.data.daily_water_summary[
sensor.controller.id
]
return daily_water_summary.total_inactive_use
def _get_controller_daily_active_water_time(sensor: HydrawiseSensor) -> float:
"""Get active water time for the controller."""
daily_water_summary = sensor.coordinator.data.daily_water_summary[
sensor.controller.id
]
return daily_water_summary.total_active_time.total_seconds()
def _get_controller_daily_total_water_use(sensor: HydrawiseSensor) -> float | None:
"""Get inactive water use for the controller."""
daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id]
daily_water_summary = sensor.coordinator.data.daily_water_summary[
sensor.controller.id
]
return daily_water_summary.total_use
CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
HydrawiseSensorEntityDescription(
key="daily_active_water_time",
translation_key="daily_active_water_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
value_fn=_get_controller_daily_active_water_time,
),
)
FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
HydrawiseSensorEntityDescription(
key="daily_total_water_use",
@ -113,6 +150,13 @@ ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.MINUTES,
value_fn=_get_zone_watering_time,
),
HydrawiseSensorEntityDescription(
key="daily_active_water_time",
translation_key="daily_active_water_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
value_fn=_get_zone_daily_active_water_time,
),
)
FLOW_MEASUREMENT_KEYS = [x.key for x in FLOW_CONTROLLER_SENSORS]
@ -129,30 +173,31 @@ async def async_setup_entry(
]
entities: list[HydrawiseSensor] = []
for controller in coordinator.data.controllers.values():
entities.extend(
HydrawiseSensor(coordinator, description, controller)
for description in CONTROLLER_SENSORS
)
entities.extend(
HydrawiseSensor(coordinator, description, controller, zone_id=zone.id)
for zone in controller.zones
for description in ZONE_SENSORS
)
entities.extend(
HydrawiseSensor(coordinator, description, controller, sensor_id=sensor.id)
for sensor in controller.sensors
for description in FLOW_CONTROLLER_SENSORS
if "flow meter" in sensor.model.name.lower()
)
entities.extend(
HydrawiseSensor(
coordinator,
description,
controller,
zone_id=zone.id,
sensor_id=sensor.id,
if coordinator.data.daily_water_summary[controller.id].total_use is not None:
# we have a flow sensor for this controller
entities.extend(
HydrawiseSensor(coordinator, description, controller)
for description in FLOW_CONTROLLER_SENSORS
)
entities.extend(
HydrawiseSensor(
coordinator,
description,
controller,
zone_id=zone.id,
)
for zone in controller.zones
for description in FLOW_ZONE_SENSORS
)
for zone in controller.zones
for sensor in controller.sensors
for description in FLOW_ZONE_SENSORS
if "flow meter" in sensor.model.name.lower()
)
async_add_entities(entities)
@ -177,6 +222,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
"""Icon of the entity based on the value."""
if (
self.entity_description.key in FLOW_MEASUREMENT_KEYS
and self.entity_description.device_class == SensorDeviceClass.VOLUME
and round(self.state, 2) == 0.0
):
return "mdi:water-outline"

View File

@ -33,6 +33,9 @@
"daily_total_water_use": {
"name": "Daily total water use"
},
"daily_active_water_time": {
"name": "Daily active watering time"
},
"daily_active_water_use": {
"name": "Daily active water use"
},
@ -43,7 +46,7 @@
"name": "Next cycle"
},
"watering_time": {
"name": "Watering time"
"name": "Remaining watering time"
}
},
"switch": {

View File

@ -187,6 +187,8 @@ def controller_water_use_summary() -> ControllerWaterUseSummary:
total_active_use=332.6,
total_inactive_use=13.0,
active_use_by_zone_id={5965394: 120.1, 5965395: 0.0},
total_active_time=timedelta(seconds=123),
active_time_by_zone_id={5965394: timedelta(seconds=123), 5965395: timedelta()},
unit="gal",
)

View File

@ -54,6 +54,55 @@
'state': '1259.0279593584',
})
# ---
# name: test_all_sensors[sensor.home_controller_daily_active_watering_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_controller_daily_active_watering_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Daily active watering time',
'platform': 'hydrawise',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'daily_active_water_time',
'unique_id': '52496_daily_active_water_time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_all_sensors[sensor.home_controller_daily_active_watering_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com',
'device_class': 'duration',
'friendly_name': 'Home Controller Daily active watering time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_controller_daily_active_watering_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.0',
})
# ---
# name: test_all_sensors[sensor.home_controller_daily_inactive_water_use-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -219,6 +268,55 @@
'state': '454.6279552584',
})
# ---
# name: test_all_sensors[sensor.zone_one_daily_active_watering_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.zone_one_daily_active_watering_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Daily active watering time',
'platform': 'hydrawise',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'daily_active_water_time',
'unique_id': '5965394_daily_active_water_time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_all_sensors[sensor.zone_one_daily_active_watering_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com',
'device_class': 'duration',
'friendly_name': 'Zone One Daily active watering time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.zone_one_daily_active_watering_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.0',
})
# ---
# name: test_all_sensors[sensor.zone_one_next_cycle-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -267,7 +365,7 @@
'state': '2023-10-04T19:49:57+00:00',
})
# ---
# name: test_all_sensors[sensor.zone_one_watering_time-entry]
# name: test_all_sensors[sensor.zone_one_remaining_watering_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@ -279,7 +377,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.zone_one_watering_time',
'entity_id': 'sensor.zone_one_remaining_watering_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@ -291,7 +389,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Watering time',
'original_name': 'Remaining watering time',
'platform': 'hydrawise',
'previous_unique_id': None,
'supported_features': 0,
@ -300,15 +398,15 @@
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_sensors[sensor.zone_one_watering_time-state]
# name: test_all_sensors[sensor.zone_one_remaining_watering_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com',
'friendly_name': 'Zone One Watering time',
'friendly_name': 'Zone One Remaining watering time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.zone_one_watering_time',
'entity_id': 'sensor.zone_one_remaining_watering_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@ -371,6 +469,55 @@
'state': '0.0',
})
# ---
# name: test_all_sensors[sensor.zone_two_daily_active_watering_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.zone_two_daily_active_watering_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Daily active watering time',
'platform': 'hydrawise',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'daily_active_water_time',
'unique_id': '5965395_daily_active_water_time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_all_sensors[sensor.zone_two_daily_active_watering_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com',
'device_class': 'duration',
'friendly_name': 'Zone Two Daily active watering time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.zone_two_daily_active_watering_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_all_sensors[sensor.zone_two_next_cycle-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -419,7 +566,7 @@
'state': 'unknown',
})
# ---
# name: test_all_sensors[sensor.zone_two_watering_time-entry]
# name: test_all_sensors[sensor.zone_two_remaining_watering_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@ -431,7 +578,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.zone_two_watering_time',
'entity_id': 'sensor.zone_two_remaining_watering_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@ -443,7 +590,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Watering time',
'original_name': 'Remaining watering time',
'platform': 'hydrawise',
'previous_unique_id': None,
'supported_features': 0,
@ -452,15 +599,15 @@
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_sensors[sensor.zone_two_watering_time-state]
# name: test_all_sensors[sensor.zone_two_remaining_watering_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com',
'friendly_name': 'Zone Two Watering time',
'friendly_name': 'Zone Two Remaining watering time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.zone_two_watering_time',
'entity_id': 'sensor.zone_two_remaining_watering_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,

View File

@ -3,7 +3,7 @@
from collections.abc import Awaitable, Callable
from unittest.mock import patch
from pydrawise.schema import Controller, User, Zone
from pydrawise.schema import Controller, ControllerWaterUseSummary, User, Zone
import pytest
from syrupy.assertion import SnapshotAssertion
@ -53,10 +53,15 @@ async def test_suspended_state(
async def test_no_sensor_and_water_state(
hass: HomeAssistant,
controller: Controller,
controller_water_use_summary: ControllerWaterUseSummary,
mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]],
) -> None:
"""Test rain sensor, flow sensor, and water use in the absence of flow and rain sensors."""
controller.sensors = []
controller_water_use_summary.total_use = None
controller_water_use_summary.total_active_use = None
controller_water_use_summary.total_inactive_use = None
controller_water_use_summary.active_use_by_zone_id = {}
await mock_add_config_entry()
assert hass.states.get("sensor.zone_one_daily_active_water_use") is None
@ -65,6 +70,18 @@ async def test_no_sensor_and_water_state(
assert hass.states.get("sensor.home_controller_daily_inactive_water_use") is None
assert hass.states.get("binary_sensor.home_controller_rain_sensor") is None
sensor = hass.states.get("sensor.home_controller_daily_active_watering_time")
assert sensor is not None
assert sensor.state == "123.0"
sensor = hass.states.get("sensor.zone_one_daily_active_watering_time")
assert sensor is not None
assert sensor.state == "123.0"
sensor = hass.states.get("sensor.zone_two_daily_active_watering_time")
assert sensor is not None
assert sensor.state == "0.0"
sensor = hass.states.get("binary_sensor.home_controller_connectivity")
assert sensor is not None
assert sensor.state == "on"