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

View File

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

View File

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

View File

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

View File

@ -187,6 +187,8 @@ def controller_water_use_summary() -> ControllerWaterUseSummary:
total_active_use=332.6, total_active_use=332.6,
total_inactive_use=13.0, total_inactive_use=13.0,
active_use_by_zone_id={5965394: 120.1, 5965395: 0.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", unit="gal",
) )

View File

@ -54,6 +54,55 @@
'state': '1259.0279593584', '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] # name: test_all_sensors[sensor.home_controller_daily_inactive_water_use-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -219,6 +268,55 @@
'state': '454.6279552584', '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] # name: test_all_sensors[sensor.zone_one_next_cycle-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -267,7 +365,7 @@
'state': '2023-10-04T19:49:57+00:00', '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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -279,7 +377,7 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'sensor', 'domain': 'sensor',
'entity_category': None, 'entity_category': None,
'entity_id': 'sensor.zone_one_watering_time', 'entity_id': 'sensor.zone_one_remaining_watering_time',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@ -291,7 +389,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Watering time', 'original_name': 'Remaining watering time',
'platform': 'hydrawise', 'platform': 'hydrawise',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -300,15 +398,15 @@
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>, '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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com', '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'>, 'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.zone_one_watering_time', 'entity_id': 'sensor.zone_one_remaining_watering_time',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
@ -371,6 +469,55 @@
'state': '0.0', '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] # name: test_all_sensors[sensor.zone_two_next_cycle-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -419,7 +566,7 @@
'state': 'unknown', 'state': 'unknown',
}) })
# --- # ---
# name: test_all_sensors[sensor.zone_two_watering_time-entry] # name: test_all_sensors[sensor.zone_two_remaining_watering_time-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -431,7 +578,7 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'sensor', 'domain': 'sensor',
'entity_category': None, 'entity_category': None,
'entity_id': 'sensor.zone_two_watering_time', 'entity_id': 'sensor.zone_two_remaining_watering_time',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@ -443,7 +590,7 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Watering time', 'original_name': 'Remaining watering time',
'platform': 'hydrawise', 'platform': 'hydrawise',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -452,15 +599,15 @@
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>, '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({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by hydrawise.com', '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'>, 'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.zone_two_watering_time', 'entity_id': 'sensor.zone_two_remaining_watering_time',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@ -3,7 +3,7 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from unittest.mock import patch from unittest.mock import patch
from pydrawise.schema import Controller, User, Zone from pydrawise.schema import Controller, ControllerWaterUseSummary, User, Zone
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -53,10 +53,15 @@ async def test_suspended_state(
async def test_no_sensor_and_water_state( async def test_no_sensor_and_water_state(
hass: HomeAssistant, hass: HomeAssistant,
controller: Controller, controller: Controller,
controller_water_use_summary: ControllerWaterUseSummary,
mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]],
) -> None: ) -> None:
"""Test rain sensor, flow sensor, and water use in the absence of flow and rain sensors.""" """Test rain sensor, flow sensor, and water use in the absence of flow and rain sensors."""
controller.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() await mock_add_config_entry()
assert hass.states.get("sensor.zone_one_daily_active_water_use") is None 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("sensor.home_controller_daily_inactive_water_use") is None
assert hass.states.get("binary_sensor.home_controller_rain_sensor") 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") sensor = hass.states.get("binary_sensor.home_controller_connectivity")
assert sensor is not None assert sensor is not None
assert sensor.state == "on" assert sensor.state == "on"