fix(axle): warn and continue when Axle component is unhealthy#4236
Conversation
A failed Axle Energy VPP integration (e.g. an expired/rotated API key that errors on every run) previously still influenced the plan: fetch.py trusted fetch_axle_sessions() and the fetch_axle_active() read-only handoff even when the component was in an error state. With stale/inconsistent Axle state this flip-flopped read-only mode and injected phantom forced import/export slots, thrashing battery charge/discharge on IOG tariffs (observed live: ~5-minutely charge<->discharge, battery 98%->49%, peak-rate import during an active IOG car session). Add Fetch.axle_component_healthy() (same 'active but not alive' definition as the components_healthy dashboard sensor) and gate both Axle influence paths on it: skip loading Axle sessions and never enter Axle read-only control when the component is unhealthy. The integration degrades to absent (warn and continue) and PredBat plans normally on the tariff. Adds unit tests for the predicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th guard
test_fetch_config_options runs against the shared my_predbat instance, whose
`components` was left as a bare _MockComponents by an earlier test (no is_active).
The new axle_control path now calls axle_component_healthy() → components.is_active
("axle"), so Test 2 crashed with AttributeError.
Inject a minimal _FakeComponents (is_active/is_alive) for the duration of the test
and restore the original in teardown. Axle is present+healthy here so the control
path is exercised; the unhealthy warn-and-continue path is covered in test_axle.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
I don't think this does anything useful, all you are doing is preventing a previous scheduled event from running if the API key becomes invalid. |
…n health The real bug behind the stale-session thrashing was in axle.py itself: publish_axle_event() only ran from inside the fetch functions' success paths (or a now-unreachable "not fetch_due" branch), so once fetching got stuck failing (e.g. an expired API key), the on/off sensor state froze forever - including a since-ended event still being reported as active - because nothing kept re-deriving it against the current time. Move publish_axle_event() out of _fetch_byok_event()/_fetch_managed_price_curve() and call it unconditionally at the end of run(), after every fetch attempt regardless of outcome. This makes the state self-correcting: a frozen cached event naturally reports "off" once its own end_time passes, with no need to know whether Axle's backend is still reachable. With that fixed at the source, the fetch.py-level axle_component_healthy() gate (and its "active but not alive" duplication of the components_healthy sensor's health formula) is no longer needed, so it's removed along with its dedicated test scaffolding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Update: moved the fix to the actual root causeThanks for the diagnosis and repro on this one — the thrashing symptom is real. Digging into why What was actually happening
Same issue on the The fix (
|
Summary
Make an unhealthy / erroring Axle Energy VPP component warn and continue instead of influencing (and thrashing) battery control.
Problem
When the Axle component errors on every run — e.g. Axle rotated their API keys and a customer's key expired —
fetch.pystill trusted Axle:fetch_axle_sessions()injected Axle VPP sessions as forced import/export slots into the plan (load_axle_slot).fetch_axle_active()could flip PredBat into read-only mode, handing battery control to the (broken) VPP.Neither path checked component health, so stale / inconsistent Axle state flip-flopped read-only mode and injected phantom forced slots — thrashing charge/discharge. Observed on a live Intelligent Octopus Go customer: ~5-minutely battery charge↔discharge, SOC 98%→49%, and peak-rate grid import during an active IOG car session, for ~2 days until the API key was regenerated.
Fix
Fetch.axle_component_healthy(), using the same "active but not alive" definition already used for thecomponents_healthydashboard sensor (predbat.py).self.axle_sessions = []) when unhealthy.Warn:and continues with the normal tariff plan — the integration degrades to absent rather than actively wrong (fail safe).A healthy or entirely absent Axle component is unaffected.
Tests
tests/test_axle.pywith unit tests for the predicate: healthy / not-configured / active-and-alive → left untouched; active-but-not-alive → ignored and warns.unit_test.py --test axle→ 29/29 pass;-k fetch_configpasses; Black / flake8 (250) / cspell clean.🤖 Generated with Claude Code