Repository navigation
fix(predbat): don't read db_primary off a None ha_interface in update_time_loop - #5137
Conversation
|
Automated review failed to complete for this PR - see the triage bot's logs for details. Not retrying automatically; remove |
|
|
||
| for loop_name in ("update_time_loop", "run_time_loop"): | ||
| # Each test gets its own PredBat, so clearing the interface here cannot reach another test | ||
| my_predbat.ha_interface = None |
There was a problem hiding this comment.
Coverage gap: this test only drives the ha_interface is None case, but the only guard branch reachable in production is the other one — interface present, websocket_active False and db_primary False (the websocket disconnect paths set websocket_active = False at ha.py:667/785, and db-primary defaults off). No existing test covers that branch either: test_active_flag sets websocket_active = True so the guard passes, and test_predheat's timer_tick drive (test_predheat.py:136) runs PredHeat's same-named update_time_loop, not PredBat's. A future edit to the condition itself (e.g. dropping the db_primary term, or swapping and/or so a db-primary install with the websocket down now trips the guard) would pass this suite. Worth one more loop pass with ha_interface kept, websocket_active = False, db_primary = False asserting the same raise + fatal_error.
| self.log("Error: HA interface not active and db_primary is {}".format(self.ha_interface.db_primary)) | ||
| # Only report db_primary when there is an interface to read it from - when ha_interface is None | ||
| # reading it here raised AttributeError before fatal_error could be set below (#5135) | ||
| if self.ha_interface: |
There was a problem hiding this comment.
This fix now leaves three hand-maintained variants of the same guard semantics: this two-branch log here, run_time_loop's shorter copy at predbat.py:2175 (different wording, no db_primary), and the None-branch message chosen to match it. The PR body documents why it didn't dedupe, and the new test pins both copies so divergence is caught — but any future change to the guard's contract (new attribute in the message, different flag, changed exception) still has to be made in two places. If another guard tweak lands, consider extracting one _check_ha_interface() helper returning a reason string that both loops log and raise from.
| # pylint: disable=attribute-defined-outside-init | ||
|
|
||
|
|
||
| def _run_guard(my_predbat, loop): |
There was a problem hiding this comment.
Minor: my_predbat is unused here — the loops are already passed as bound methods, so def _run_guard(loop) does the same job. (Same shape as _run_loop_expecting_failure in test_active_flag.py, so this is only worth tidying if that helper is touched too.)
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The fix is narrowly scoped to the failing branch and is covered by a focused regression test that reproduces the original defect and verifies the intended behavior.
Review effort: Lite
Findings: None
What changed in this PR
Fixes a crash in PredBat.update_time_loop() when ha_interface is None, ensuring the HA-interface guard reliably sets fatal_error and raises the intended "HA interface not active" exception instead of failing early with AttributeError.
Changes:
- Guard log in
update_time_loopnow only reads/logsdb_primarywhenha_interfaceis present. - Added a regression test covering both
update_time_loopandrun_time_loopbehavior whenha_interfaceisNone. - Registered the new test in the unit test registry.
| File | Description |
|---|---|
| apps/predbat/predbat.py | Prevents AttributeError in the HA-interface guard by conditionally logging db_primary only when ha_interface exists. |
| apps/predbat/tests/test_time_loop_guard.py | Adds a regression test ensuring both loops raise the correct exception and set fatal_error when ha_interface is missing. |
| apps/predbat/unit_test.py | Registers the new time_loop_guard test in the suite. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This is an automated draft PR generated from issue #5135 — a maintainer should review it before merging.
Fixes #5135
Summary
update_time_loop's HA-interface guard loggedself.ha_interface.db_primaryunconditionally, so in the one case the guard exists for —ha_interfacebeingNone, which short-circuits the first clause — the log line raisedAttributeErrorbeforeself.fatal_error = Trueon the next line ever ran. The flag that tells standalone mode to stop Predbat (hass.py:159) therefore stayedFalse.db_primaryis now only reported when there is an interface to read it from; with no interface the message matchesrun_time_loop's existing wording ("Error: HA interface not active"), and the flag is set either way.Testing
coverage/run_pre_commit— all hooks pass (ruff, black, cspell, interrogate, markdownlint) and the quick suite is green, exit 0.apps/predbat/tests/test_time_loop_guard.py, registered astime_loop_guardinTEST_REGISTRY. It clearsha_interfaceand drives bothupdate_time_loopandrun_time_loop, asserting each raises the intendedHA interface not activeexception (notAttributeError) and leavesfatal_errorset.tools/triage_test.sh time_loop_guardfails on exactly the reported defect —ERROR: update_time_loop raised AttributeError ('NoneType' object has no attribute 'db_primary')followed byERROR: update_time_loop did not set fatal_error, whilerun_time_loopalready passed. With the fix restored it passes, all four assertions OK.Notes
run_time_loopkeeps its own copy of the same guard and message rather than sharing a helper withupdate_time_loop. That duplication pre-dates this change and de-duplicating it would touch the scheduled-run path, so it is left alone; the new test covers both copies so a future divergence is caught.impact({target: "update_time_loop", direction: "upstream"}): LOW risk, one direct caller (initialize, which only schedules it). No behaviour change outside theha_interface is Nonebranch.initialize()raises atpredbat.py:2002ifha_interfaceis unset, before the loop is scheduled — so the fix is about the guard being able to do its job, not a reproduction from a live report.