Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
40 changes: 26 additions & 14 deletions custom_components/bhyve/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,31 @@ async def fetch_zone_landscape(

return landscapes

@staticmethod
def _apply_watering_in_progress(
device_data: dict[str, Any], event_data: dict[str, Any]
) -> None:
"""Apply a watering_in_progress_notification event to device state."""
status = device_data.setdefault("status", {})
current_station = event_data.get("current_station")
run_time = event_data.get("run_time")
stations = event_data.get("stations") or []
# The event provides run_time at the top level without a stations array.
# Synthesize one so the shape matches the API response and
# current_runtime stays populated for valve entities.
if not stations and run_time is not None and current_station is not None:
stations = [{"station": current_station, "run_time": run_time}]
status["watering_status"] = {
"current_station": current_station,
"program": event_data.get("program"),
"run_time": run_time,
"started_watering_station_at": event_data.get(
"started_watering_station_at"
),
"stations": stations,
}
status["run_mode"] = event_data.get("mode", "manual")

async def async_handle_device_event(self, event_data: dict[str, Any]) -> None: # noqa: PLR0912
"""Handle WebSocket device events."""
if not self.data:
Expand Down Expand Up @@ -234,20 +259,7 @@ async def async_handle_device_event(self, event_data: dict[str, Any]) -> None:
del device_data["status"]["watering_status"]

elif event == EVENT_WATERING_IN_PROGRESS:
# Update watering status
if "status" not in device_data:
device_data["status"] = {}
# Store the full watering status from the event
device_data["status"]["watering_status"] = {
"current_station": event_data.get("current_station"),
"program": event_data.get("program"),
"run_time": event_data.get("run_time"),
"started_watering_station_at": event_data.get(
"started_watering_station_at"
),
"stations": event_data.get("stations", []),
}
device_data["status"]["run_mode"] = event_data.get("mode", "manual")
self._apply_watering_in_progress(device_data, event_data)

elif event == EVENT_WATERING_COMPLETE:
# Clear watering status
Expand Down
35 changes: 35 additions & 0 deletions tests/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,38 @@ async def test_handle_program_update_unknown_program(
# Verify no changes and no update triggered
assert "unknown-program-id" not in coordinator.data["programs"]
mock_update.assert_not_called()


class TestDeviceEventHandling:
"""Test device event handling in coordinator."""

async def test_handle_watering_in_progress_synthesizes_stations(
self, hass: HomeAssistant
) -> None:
"""
Synthesize a stations entry from top-level run_time.

The `watering_in_progress_notification` WebSocket event provides
`run_time` at the top level but does not include a `stations` array.
Downstream entities read runtime from `watering_status.stations[0]`,
so the coordinator must populate it so `current_runtime` is not lost.
"""
coordinator = create_mock_coordinator(hass)

event_data = {
"event": "watering_in_progress_notification",
"device_id": TEST_DEVICE_ID,
"program": "e",
"current_station": 1,
"run_time": 14,
"started_watering_station_at": "2020-01-09T20:29:59.000Z",
}

with patch.object(coordinator, "async_set_updated_data"):
await coordinator.async_handle_device_event(event_data)

watering_status = coordinator.data["devices"][TEST_DEVICE_ID]["device"][
"status"
]["watering_status"]
assert watering_status["current_station"] == 1
assert watering_status["stations"] == [{"station": 1, "run_time": 14}]
Loading