Skip to content

Commit a4bb95d

Browse files
FlorianCasseFlorian Casse
andauthored
Fix false stove disconnects (#5)
Co-authored-by: Florian Casse <fcasse@itq.eu>
1 parent b55187e commit a4bb95d

5 files changed

Lines changed: 29 additions & 15 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ All entities are grouped under a single **Duepi Pellet Stove** device:
115115
| Online | Binary sensor | Whether the stove is reachable via dpremoteiot.com |
116116
| Power level | Number | Slider to adjust power level (1-5) |
117117

118+
The cloud can briefly report a healthy stove offline during its periodic heartbeat.
119+
The integration waits 120 seconds before publishing a disconnect, while restoring
120+
connectivity immediately on the next online report.
121+
118122
## Migration from Script-Based Setup
119123

120124
If you were using the previous `stoveOnOff.py` + `command_line` setup, an interactive migration script is provided to guide you through the process.

‎custom_components/duepi/coordinator.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828

2929
_LOGGER = logging.getLogger(__name__)
3030

31-
DISCONNECT_GRACE_PERIOD = 60
31+
# The cloud periodically reports healthy stoves offline for 60-90 seconds.
32+
# Wait long enough to absorb that upstream heartbeat gap while still exposing
33+
# sustained disconnects promptly.
34+
DISCONNECT_GRACE_PERIOD = 120
3235

3336

3437
class DuepiCoordinator(DataUpdateCoordinator[DuepiStoveState]):

‎custom_components/duepi/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "duepi",
33
"name": "Duepi Pellet Stove",
4-
"version": "1.2.0",
4+
"version": "1.2.1",
55
"config_flow": true,
66
"documentation": "https://github.com/FlorianCasse/DuepiRemoteHA",
77
"issue_tracker": "https://github.com/FlorianCasse/DuepiRemoteHA/issues",

‎tests/test_connectivity.py‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,30 +73,37 @@ def poll_states(
7373
return results
7474

7575

76-
def test_false_readings_within_grace_period_keep_stove_online(
76+
def test_observed_ninety_second_false_window_keeps_stove_online(
7777
monkeypatch: pytest.MonkeyPatch, duepi_test_modules: SimpleNamespace
7878
) -> None:
79-
"""A transient offline report after a connected report must not flap availability."""
79+
"""The periodic 60-90 second cloud false window must not flap availability."""
8080
results = poll_states(
8181
monkeypatch,
8282
duepi_test_modules,
8383
[
8484
stove_state(duepi_test_modules.api, True),
8585
stove_state(duepi_test_modules.api, False),
8686
stove_state(duepi_test_modules.api, False),
87+
stove_state(duepi_test_modules.api, False),
8788
stove_state(duepi_test_modules.api, True),
8889
],
89-
[0, 1, 30, 31],
90+
[0, 1, 31, 61, 91],
9091
)
9192

92-
assert [state.online for state in results] == [True, True, True, True]
93-
assert [state.raw_online for state in results] == [True, False, False, True]
93+
assert [state.online for state in results] == [True, True, True, True, True]
94+
assert [state.raw_online for state in results] == [
95+
True,
96+
False,
97+
False,
98+
False,
99+
True,
100+
]
94101

95102

96-
def test_continuous_offline_for_sixty_seconds_confirms_disconnect(
103+
def test_continuous_offline_for_two_minutes_confirms_disconnect(
97104
monkeypatch: pytest.MonkeyPatch, duepi_test_modules: SimpleNamespace
98105
) -> None:
99-
"""A continuous raw offline period becomes published offline at 60 seconds."""
106+
"""A continuous raw offline period becomes published offline at 120 seconds."""
100107
results = poll_states(
101108
monkeypatch,
102109
duepi_test_modules,
@@ -106,7 +113,7 @@ def test_continuous_offline_for_sixty_seconds_confirms_disconnect(
106113
stove_state(duepi_test_modules.api, False),
107114
stove_state(duepi_test_modules.api, False),
108115
],
109-
[0, 1, 30, 61],
116+
[0, 1, 61, 121],
110117
)
111118

112119
assert [state.online for state in results] == [True, True, True, False]
@@ -129,7 +136,7 @@ def test_offline_is_published_at_grace_deadline_without_another_poll(
129136
assert [state.online for state in results] == [True, True]
130137
assert len(duepi_test_modules.scheduler.calls) == 1
131138
delayed_call = duepi_test_modules.scheduler.calls[0]
132-
assert delayed_call.delay == 60
139+
assert delayed_call.delay == 120
133140

134141
duepi_test_modules.scheduler.fire(delayed_call)
135142

@@ -285,7 +292,7 @@ def test_disconnect_confirmation_is_logged_once_when_polling_resumes(
285292
clock["now"] = 1
286293
coordinator.async_set_updated_data(asyncio.run(coordinator._async_update_data()))
287294
duepi_test_modules.scheduler.fire(duepi_test_modules.scheduler.calls[0])
288-
clock["now"] = 90
295+
clock["now"] = 121
289296
coordinator.async_set_updated_data(asyncio.run(coordinator._async_update_data()))
290297

291298
assert coordinator.data.online is False
@@ -319,7 +326,7 @@ def test_raw_online_restores_connectivity_immediately(
319326
stove_state(duepi_test_modules.api, True),
320327
stove_state(duepi_test_modules.api, False),
321328
],
322-
[0, 1, 61, 62, 63],
329+
[0, 1, 121, 122, 123],
323330
)
324331

325332
assert [state.online for state in results] == [True, True, False, True, True]
@@ -343,7 +350,7 @@ def test_unknown_report_does_not_hide_a_later_reconnect(
343350
stove_state(duepi_test_modules.api, None),
344351
stove_state(duepi_test_modules.api, True),
345352
],
346-
[0, 1, 61, 62, 63],
353+
[0, 1, 121, 122, 123],
347354
)
348355

349356
assert [state.online for state in results] == [True, True, False, None, True]

‎tests/test_integration_regressions.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def test_diagnostics_redacts_all_identifiers_and_exposes_connectivity_timing(
172172
"raw_online": False,
173173
"filtered_online": True,
174174
"disconnect_elapsed_seconds": 0,
175-
"grace_period_seconds": 60,
175+
"grace_period_seconds": 120,
176176
}
177177
assert result["last_successful_update"] == "2026-07-13T10:00:00+00:00"
178178
assert result["last_error"] == "RuntimeError"

0 commit comments

Comments
 (0)