Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report total watering time as part of the water use summary #165

Merged
merged 6 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 71 additions & 39 deletions pydrawise/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Client library for interacting with Hydrawise's cloud API."""

from datetime import datetime
from datetime import datetime, timedelta
from functools import cache
from importlib import resources
import logging
Expand All @@ -16,6 +16,7 @@
from .schema import (
Controller,
ControllerWaterUseSummary,
CustomSensorTypeEnum,
DateTime,
LocalizedValueType,
Sensor,
Expand Down Expand Up @@ -391,25 +392,45 @@ async def get_water_use_summary(
:param controller: The controller whose water use to report.
:param start: Start time
:param end: End time."""
selector = self._schema.Query.controller(controllerId=controller.id).select(
self._schema.Controller.sensors.select(
*get_selectors(self._schema, Sensor),
self._schema.Sensor.flowSummary(
start=int(start.timestamp()),
end=int(end.timestamp()),
).select(*get_selectors(self._schema, SensorFlowSummary)),
),
has_flow_sensors = controller.sensors and (
len(
list(
filter(
lambda sensor: sensor.model.sensor_type
is CustomSensorTypeEnum.FLOW,
controller.sensors,
)
)
)
> 0
)
thomaskistler marked this conversation as resolved.
Show resolved Hide resolved
selectors = [
# Request the watering report that contains both the
# amount of water used as well as the watering time.
self._schema.Controller.reports.select(
self._schema.Reports.watering(
**{
"from": int(start.timestamp()),
"until": int(end.timestamp()),
}
).select(*get_selectors(self._schema, WateringReportEntry)),
),
)
]
if has_flow_sensors:
# Only request the flow summary in the presence of flow sensors
selectors.append(
self._schema.Controller.sensors.select(
*get_selectors(self._schema, Sensor),
self._schema.Sensor.flowSummary(
start=int(start.timestamp()),
end=int(end.timestamp()),
).select(*get_selectors(self._schema, SensorFlowSummary)),
)
)
selector = self._schema.Query.controller(controllerId=controller.id).select(
*selectors
)
result = await self._query(selector)
summary = ControllerWaterUseSummary()

# watering report entries
entries = _prune_watering_report_entries(
Expand All @@ -420,36 +441,47 @@ async def get_water_use_summary(
end,
)

# total active water use
# total active water use and time
summary = ControllerWaterUseSummary()
for entry in entries:
if (
entry.run_event is not None
and entry.run_event.zone is not None
and entry.run_event.reported_water_usage is not None
):
active_use = entry.run_event.reported_water_usage.value
if summary.unit == "":
summary.unit = entry.run_event.reported_water_usage.unit
summary.total_active_use += active_use
summary.active_use_by_zone_id.setdefault(entry.run_event.zone.id, 0)
summary.active_use_by_zone_id[entry.run_event.zone.id] += active_use
if entry.run_event is not None and entry.run_event.zone is not None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if entry.run_event is not None and entry.run_event.zone is not None:
if entry.run_event is None or entry.run_event.zone is None:
continue

if (
entry.run_event.reported_water_usage is not None
and has_flow_sensors
):
active_use = entry.run_event.reported_water_usage.value
if summary.unit == "":
summary.unit = entry.run_event.reported_water_usage.unit
summary.total_active_use += active_use
summary.active_use_by_zone_id.setdefault(entry.run_event.zone.id, 0)
summary.active_use_by_zone_id[entry.run_event.zone.id] += active_use

active_time = entry.run_event.reported_duration
summary.total_active_time += active_time
summary.active_time_by_zone_id.setdefault(
entry.run_event.zone.id, timedelta()
)
summary.active_time_by_zone_id[entry.run_event.zone.id] += active_time

# total active and inactive water use
for sensor in result["controller"]["sensors"]:
if (
"FLOW" in sensor["model"]["sensorType"]
and "flowSummary" in sensor
and (flow_summary := sensor["flowSummary"]) is not None
):
summary.total_use += flow_summary["totalWaterVolume"]["value"]
if summary.unit == "":
summary.unit = flow_summary["totalWaterVolume"]["unit"]

# Correct for inaccuracies. The watering report and flow summaries are not always
# updated with the same frequency.
if summary.total_use > summary.total_active_use:
summary.total_inactive_use = summary.total_use - summary.total_active_use
else:
summary.total_use = summary.total_active_use
if has_flow_sensors:
thomaskistler marked this conversation as resolved.
Show resolved Hide resolved
for sensor in result["controller"]["sensors"]:
if (
"FLOW" in sensor["model"]["sensorType"]
and "flowSummary" in sensor
and (flow_summary := sensor["flowSummary"]) is not None
):
summary.total_use += flow_summary["totalWaterVolume"]["value"]
if summary.unit == "":
summary.unit = flow_summary["totalWaterVolume"]["unit"]
thomaskistler marked this conversation as resolved.
Show resolved Hide resolved

# Correct for inaccuracies. The watering report and flow summaries are not always
# updated with the same frequency.
if summary.total_use > summary.total_active_use:
summary.total_inactive_use = (
summary.total_use - summary.total_active_use
)
else:
summary.total_use = summary.total_active_use

return summary
8 changes: 7 additions & 1 deletion pydrawise/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,13 +665,19 @@ class ControllerWaterUseSummary:

Active use means water use during a scheduled or manual zone run.
Inactive use means water use when no zone was actively running. This can happen when
faucets (i.e., garden hoses) are installed downstream of the flow meter.
faucets (i.e., garden hoses) are installed downstream of the flow meter. Water use
is only reported in the presence of a flow sensor. Active watering time is always
reported.
"""

_pydrawise_type = True

total_use: float = 0.0
total_active_use: float = 0.0
total_inactive_use: float = 0.0
thomaskistler marked this conversation as resolved.
Show resolved Hide resolved
total_active_time: timedelta = field(
metadata=_duration_conversion("seconds"), default=timedelta()
)
active_use_by_zone_id: dict[int, float] = field(default_factory=dict)
active_time_by_zone_id: dict[int, timedelta] = field(default_factory=dict)
unit: str = ""
65 changes: 51 additions & 14 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,33 +249,40 @@ def watering_report_json():
"reportedCurrent": {"value": 280, "unit": "mA"},
}
},
]
}


@fixture
def watering_report_without_sensor_json():
yield {
"watering": [
{
"runEvent": {
"id": "35220026903",
"id": "35220026902",
"zone": {
"id": 5955345,
"number": {"value": 2, "label": "Zone 2"},
"name": "Front Trees",
"id": 5955343,
"number": {"value": 1, "label": "Zone 1"},
"name": "Front Lawn",
},
"standardProgram": None,
"advancedProgram": {"id": 4729362, "name": ""},
"standardProgram": {
"id": 343434,
"name": "",
},
"advancedProgram": {"id": 4729361, "name": ""},
"reportedStartTime": {
"value": "Fri, 01 Nov 23 04:19:59 -0800",
"timestamp": 1698797999,
"value": "Fri, 01 Dec 23 04:00:00 -0800",
"timestamp": 1701432000,
},
"reportedEndTime": {
"value": "Fri, 01 Nov 23 04:39:59 -0800",
"timestamp": 1698799199,
"value": "Fri, 01 Dec 23 04:20:00 -0800",
"timestamp": 1701433200,
},
"reportedDuration": 1200,
"reportedStatus": {
"value": 1,
"label": "Normal watering cycle",
},
"reportedWaterUsage": {
"value": 49.00048126864295,
"unit": "gal",
},
"reportedStopReason": {
"finishedNormally": True,
"description": ["Finished normally"],
Expand Down Expand Up @@ -581,8 +588,38 @@ async def test_get_water_use_summary(
assert "watering" in query
assert "flowSummary(" in query
assert summary.active_use_by_zone_id[5955343] == 34.000263855044786
assert summary.active_time_by_zone_id[5955343] == timedelta(seconds=1200)
assert summary.total_active_use == 34.000263855044786
assert summary.total_inactive_use == (
23100.679266065246 if flow_summary_json else 0.0
)
assert summary.total_active_time == timedelta(seconds=1200)
assert summary.unit == "gal"


async def test_get_water_use_summary_without_sensor(
api: Hydrawise,
mock_session,
controller_json,
watering_report_without_sensor_json,
):
mock_session.execute.return_value = {
"controller": {
"reports": watering_report_without_sensor_json,
}
}
ctrl = deserialize(Controller, controller_json)
ctrl.sensors = None
summary = await api.get_water_use_summary(
ctrl, datetime(2023, 12, 1, 0, 0, 0), datetime(2023, 12, 4, 0, 0, 0)
)
mock_session.execute.assert_awaited_once()
[selector] = mock_session.execute.await_args.args
query = print_ast(selector)
assert "reports" in query
assert "watering" in query
assert 5955343 not in summary.active_use_by_zone_id
assert summary.active_time_by_zone_id[5955343] == timedelta(seconds=1200)
assert summary.total_active_use == 0.0
assert summary.total_inactive_use == 0.0
assert summary.total_active_time == timedelta(seconds=1200)