Skip to content

Commit d3f1bd2

Browse files
authored
Add regression tests for issue #306 zone toggle during program run (#399)
The old switch-based zone entity closed itself on a `change_mode` event with `mode=auto`, which the B-hyve cloud emits right after `watering_in_progress_notification` when a program starts. The migration to valve entities (PR #240) moved state derivation to the coordinator, but the scenario was never pinned by tests. Replay the event sequence from the issue log against the real coordinator and assert the valve stays open across the interleaved `change_mode` and closes cleanly on `device_idle`.
1 parent 05c975a commit d3f1bd2

2 files changed

Lines changed: 150 additions & 1 deletion

File tree

tests/test_coordinator.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,75 @@ async def test_handle_watering_in_progress_synthesizes_stations(
232232
]["watering_status"]
233233
assert watering_status["current_station"] == 1
234234
assert watering_status["stations"] == [{"station": 1, "run_time": 14}]
235+
236+
async def test_change_mode_auto_preserves_watering_status(
237+
self, hass: HomeAssistant
238+
) -> None:
239+
"""
240+
Issue #306: change_mode auto must not clear active watering_status.
241+
242+
Reproduces the event sequence from the user's log:
243+
1. watering_in_progress_notification arrives (zone 1 starts watering)
244+
2. change_mode with mode=auto arrives ~1s later
245+
246+
Previously, when zone entities were switches, the change_mode handler
247+
set is_on=False on all zone switches, producing the on/off toggle
248+
reported in the issue. Verify that the coordinator does not wipe the
249+
watering_status on change_mode so valve entities stay open.
250+
"""
251+
coordinator = create_mock_coordinator(hass)
252+
253+
watering_event = {
254+
"event": "watering_in_progress_notification",
255+
"device_id": TEST_DEVICE_ID,
256+
"program": "a",
257+
"current_station": 1,
258+
"run_time": 49.98,
259+
"total_run_time_sec": 3000,
260+
"started_watering_station_at": "2025-05-27T10:00:03.000Z",
261+
"timestamp": "2025-05-27T10:00:04.000Z",
262+
"status": "watering_in_progress",
263+
"rain_sensor_hold": False,
264+
}
265+
change_mode_event = {
266+
"event": "change_mode",
267+
"mode": "auto",
268+
"device_id": TEST_DEVICE_ID,
269+
"timestamp": "2025-05-27T10:00:04.000Z",
270+
}
271+
272+
with patch.object(coordinator, "async_set_updated_data"):
273+
await coordinator.async_handle_device_event(watering_event)
274+
await coordinator.async_handle_device_event(change_mode_event)
275+
276+
status = coordinator.data["devices"][TEST_DEVICE_ID]["device"]["status"]
277+
assert "watering_status" in status
278+
assert status["watering_status"]["current_station"] == 1
279+
assert status["watering_status"]["program"] == "a"
280+
281+
async def test_device_idle_clears_watering_status(
282+
self, hass: HomeAssistant
283+
) -> None:
284+
"""Device_idle must clear watering_status so the valve closes."""
285+
coordinator = create_mock_coordinator(hass)
286+
287+
with patch.object(coordinator, "async_set_updated_data"):
288+
await coordinator.async_handle_device_event(
289+
{
290+
"event": "watering_in_progress_notification",
291+
"device_id": TEST_DEVICE_ID,
292+
"program": "a",
293+
"current_station": 1,
294+
"run_time": 14,
295+
}
296+
)
297+
await coordinator.async_handle_device_event(
298+
{
299+
"event": "device_idle",
300+
"device_id": TEST_DEVICE_ID,
301+
}
302+
)
303+
304+
status = coordinator.data["devices"][TEST_DEVICE_ID]["device"]["status"]
305+
assert "watering_status" not in status
306+
assert status["run_mode"] == "off"

tests/test_valve.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Test BHyve valve entities."""
22

3-
from unittest.mock import AsyncMock, MagicMock
3+
from unittest.mock import AsyncMock, MagicMock, patch
44

55
import pytest
6+
from homeassistant.core import HomeAssistant
67

78
from custom_components.bhyve.coordinator import BHyveDataUpdateCoordinator
89
from custom_components.bhyve.pybhyve.typings import BHyveDevice, BHyveZone
@@ -444,3 +445,79 @@ async def test_valve_set_manual_preset_runtime(
444445
coordinator.client.set_manual_preset_runtime.assert_called_once_with(
445446
mock_sprinkler_device["id"], 8
446447
)
448+
449+
450+
async def test_valve_does_not_toggle_on_change_mode_during_watering(
451+
hass: HomeAssistant,
452+
mock_sprinkler_device: BHyveDevice,
453+
mock_zone_data: BHyveZone,
454+
) -> None:
455+
"""
456+
Regression for issue #306: valve must not close mid-watering.
457+
458+
The B-hyve cloud interleaves a `change_mode` (mode=auto) event right after
459+
`watering_in_progress_notification` when a program starts. In the old
460+
switch-based implementation this flipped the zone switch off/on and showed
461+
up as a brief toggle. Drive the real coordinator through that event
462+
sequence and assert the valve stays open the whole time.
463+
"""
464+
client = MagicMock()
465+
entry = MagicMock()
466+
entry.entry_id = "test_entry"
467+
coordinator = BHyveDataUpdateCoordinator(hass, client, entry)
468+
device_id = mock_sprinkler_device["id"]
469+
coordinator.data = {
470+
"devices": {
471+
device_id: {
472+
"device": dict(mock_sprinkler_device),
473+
"history": [],
474+
"landscapes": {},
475+
}
476+
},
477+
"programs": {},
478+
}
479+
coordinator.client = MagicMock()
480+
coordinator.client.send_message = AsyncMock()
481+
482+
valve = BHyveZoneValve(
483+
coordinator=coordinator,
484+
device=coordinator.data["devices"][device_id]["device"],
485+
zone=mock_zone_data,
486+
zone_name="Front Yard",
487+
device_programs=[],
488+
)
489+
490+
assert valve.is_closed is True
491+
492+
with patch.object(coordinator, "async_set_updated_data"):
493+
await coordinator.async_handle_device_event(
494+
{
495+
"event": "watering_in_progress_notification",
496+
"device_id": device_id,
497+
"program": "a",
498+
"current_station": "1",
499+
"run_time": 49.98,
500+
"total_run_time_sec": 3000,
501+
"started_watering_station_at": "2025-05-27T10:00:03.000Z",
502+
}
503+
)
504+
assert valve.is_closed is False, "valve should open when watering starts"
505+
506+
await coordinator.async_handle_device_event(
507+
{
508+
"event": "change_mode",
509+
"mode": "auto",
510+
"device_id": device_id,
511+
}
512+
)
513+
assert valve.is_closed is False, (
514+
"valve must stay open after change_mode mode=auto (issue #306)"
515+
)
516+
517+
await coordinator.async_handle_device_event(
518+
{
519+
"event": "device_idle",
520+
"device_id": device_id,
521+
}
522+
)
523+
assert valve.is_closed is True, "valve should close when device_idle arrives"

0 commit comments

Comments
 (0)