Repository navigation
fix(output): show History state under a warning, and don't read warnings as charge/export states - #5271
Merged
Conversation
A warning or error recorded with had_errors pins itself on predbat.status for the rest of the run, and update_pred() then only logs the real run status. A warning that recurs every cycle - "Return bad float value unavailable from car_charging_soc" after a car integration logs out - therefore replaces the recorded state for as long as it lasts. calculate_yesterday() classified statuses by substring, so "car_charging_soc" matched "charging" and a day of force exports was shown as HoldChrg, with the target set to the SoC actually reached. Map Warn:/Error: statuses to an empty state that matches neither side, unwrapping the component-error summary to the run status it carries. Hours covered by a pinned warning now show no state rather than a wrong one; preserving the real run status alongside the warning is a separate change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A warning or error recorded during a run takes over predbat.status, and the had_errors branch of record_final_run_status() only logged the state the run executed. A warning that recurs every cycle therefore erased that state from the sensor, and from its history, for as long as it lasted. End such a run by putting its state in front of the warning, in the same comma form as ", Hold for car": "Exporting, Warn: ...". The state changes with either half, so Home Assistant's history keeps it; attribute-only changes would be filtered out. The warning's own debug is kept, error_count is not counted again and no notification is sent. A run whose had_errors came from a component thread with no warning recorded leaves the sensor as before, and a run that bails out early keeps its bare error, since nothing was executed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…flag Split each historical predbat.status into the state it records and the warning it carries. "Exporting, Warn: ..." now rebuilds as the export it was (force and freeze exports stay distinct) instead of being matched by substring against the warning text, and the slot is flagged with a red flag in the state cell plus a status_warning reason naming the problem, after the slot's own reason. A bare warning, from a run that executed nothing or from history recorded before the state was kept, still rebuilds as no state, and is flagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A warning recorded mid-run was written bare, then with the run's state in front of it at the end, so a warning that recurs every cycle changed the status sensor's state twice a cycle. A warning whose text repeats the one already shown now keeps that state in front of it. Any other warning or error, including a run bailing out without executing, is still shown bare - so two different recurring warnings, or one whose text changes each cycle, still change state within a cycle, as they did before. Also stop reading Predheat's bare "Warn - ..." as a charge/export state in the History view, and document the status format and the History red flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…istory parser split_status_warning() re-stated the "Error: Complete run status ... with component errors: ..." literal that record_final_run_status() builds, in a different file. A wording change on either side would have silently lost the run's state from every History slot recorded under a component error. Both now read the prefix and suffix from const.py, and the component health test round-trips the real summary through the parser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est-only helper split_status_warning() knew Predheat's bare "Warn - ..." form but not the same warning behind a state, so "Exporting, Warn - ..." would have been read as one state and lost its flag. The marker list now matches the bare list. yesterday_status_core() had no production caller once calculate_yesterday() moved to split_status_warning(); the test now calls split_status_warning() directly and checks the warning half too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esn't trust The slot reconstruction ignores the first or last five minutes of a slot when they don't hold one state, as that is the previous (or next) slot's status lagging the boundary. The warning flag scanned the whole slot, so a warning that lagged in with that leftover flagged a slot whose shown state was rebuilt without those minutes. Warnings in an untrusted edge are now dropped along with the status they arrived with. Also notes why a warning is held across a recorder gap: it is held with the same forward-filled status the slot's state is rebuilt from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Component threads (GE Cloud, Solis, ...) call record_status() alongside the main loop. The error_count attribute is a read-modify-write of the sensor's own state, so two concurrent calls could both read N and both write N+1. record_status() and record_status_under_warning() now share a status lock, which also keeps each call's current_status/status_warning update whole. It does not cover record_final_run_status()'s choice between them, which reads had_errors outside the lock: a component warning landing between that read and the write is still overwritten until the next run, as before this change. The status-change notification is sent after the lock is released: over the websocket it waits on another thread for up to two minutes, which would otherwise stall every other status write. The lock is kept out of the debug dump, which serialises PredBat's attributes and can't pickle a lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chalfontchubby
force-pushed
the
fix/history-ignore-warning-status
branch
from
September 28, 2026 15:09
8155d23 to
0298b14
Compare
chalfontchubby
marked this pull request as ready for review
September 28, 2026 19:39
split_status_warning() tried ', warn:' before ', error:' and so on, rather than taking whichever came first. A status like 'Charging, Error: ... , Warn: ...' was cut at the later marker, so the flag's tooltip showed only the tail of the error and the state part kept the error text. Take the minimum index across all markers instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reeze automations With this branch, a run that raised a warning ends with predbat.status set to '<state>, Warn: ...'. That gets past the Freeze Charge Exit automation's Warn:/Error: filter, and its exact match on 'Hold charging' then fails, so the default branch turns AC charge off during a hold. Match with startswith, as the same automations already do for 'Charging' and 'Freeze charging'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rning-status Conflicts resolved: - apps/predbat/const.py: kept both new constants - this branch's COMPONENT_ERROR_STATUS_PREFIX/SUFFIX and main's REPEAT_FULL_LOG_SECONDS (#5428). - apps/predbat/predbat.py: const import takes both sides' additions. - apps/predbat/output.py: const import keeps this branch's multi-line form plus main's new RUN_EVERY (used by history_slot/history_refresh_minutes). The calculate_yesterday() publish_html_plan call auto-merged with both status_warnings and main's refresh_minutes. - docs/inverter-setup.md: took main's reformatted LuxPower freeze automations and re-applied this branch's four 'Hold charging' exact matches as startswith('Hold charging'). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tatus stubs
main now sets plan_json["refresh_minutes"] on both plans calculate_yesterday()
builds, so the stubbed plan_write_debug/publish_html_plan must return a dict
like the other stubs in this file, not the string "{}".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Route C (merged from main) has a Node-RED flow follow predbat.status by exact value. With this branch a run that raised a warning records '<state>, Warn: ...', which an exact match sends to the catch-all row - the same trap the LuxPower freeze automations were changed for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update_pred() cleared had_errors and the recorded warning without taking status_lock. A component thread recording a warning between the two resets left had_errors set with the warning text cleared, so the end of the run could not put its state back in front of that warning and the sensor kept the bare warning until the next run. The resets now live in reset_run_status(), which takes the lock as record_status() does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
record_status_under_warning() put the run's state back in front of the warning but wrote an empty detail, so the target SoC that a clean run shows (dashboard and gateway read the sensor's detail attribute) disappeared for as long as a warning recurred. It now takes the run's status_extra and writes it as the detail, as a clean run does. A repeated mid-run warning that keeps the previous state in front of it now keeps that state's detail too. The check reads the sensor state as last written (status_state) rather than current_status, which carries the detail on the end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rning The midnight tariff comparison runs after record_final_run_status() and re-reads sensors and rates, so it can record a warning of its own (a bad rate in a compared tariff, say) or a different one than the run raised. That warning took over predbat.status bare until the next run, so the sensor showed no state - which a status-following automation reads as "anything else". run_comparison() now takes the run's status and, when the comparison changed the sensor, puts that state and its detail back in front of the warning, as for a warning raised during the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watchdog cancels Freeze charging if predbat.status does not start with 'Freeze charging' 30 seconds after the guard is armed. A warning recorded part way through a plan update is shown bare until the run puts its state back in front of it, so a watchdog check landing in that window cancelled a freeze Predbat was carrying out. It now skips a bare Warn:/Error: status (and unknown/unavailable), as the exit automation already does; the state written after the warning changes the status, so the exit automation still cleans up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onent error While a component is failing, predbat.status reads "Error: Complete run status <state> with component errors: ..." at the end of every update, not just for a moment. Ignoring it as a bare Error: meant a freeze that Predbat had turned into Hold charging was never cancelled: the exit automation ignores Error: statuses too, and the status does not change. The watchdog now takes the state out of that form before checking it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clamp and concurrency tests reset current_status but not the status_state/status_detail pair that record_status() now checks for a repeated warning, so they left them changed on the shared PredBat fixture for whichever test runs next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…very check The previous commit read the state out of the component-error status for the abort check only. The AC and SoC steps after it still tested the raw status, so with a component failing a watchdog that now got past the abort turned grid AC charging off and left the freeze charge level in place while Predbat reported Charging or Hold charging. The state is now read out once into a variable and every check uses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
utils.py: the debug-dump exclusion list keeps both this branch's status_lock and main's io_slot_log_gate (#5569), with the comment covering both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… brackets
predbat.status for a run that raised a warning is now "Warn: ... [Exporting]"
rather than "Exporting, Warn: ...". The status keeps starting with the warning,
so the web dashboard, the gateway (and predbat.com behind it) and user templates
that check startswith('Warn') or startswith('Error') behave exactly as before;
only the History view reads the state back out.
- split_status_warning() takes the state from a trailing bracket matched by
balance from the end, as the state carries brackets of its own ("Demand
[Freeze exporting]", "Charging [Alert]"), and only when it starts with a
known run state (const.RUN_STATE_NAMES) - warning text can end in brackets
that are not a state, e.g. an interpolated list.
- The 255-character clamp shortens the warning, never the state suffix.
- A warning repeating mid-run still keeps the suffix, matched by parsing the
shown status rather than by its ending.
- The LuxPower and Victron route C doc changes are reverted: with the warning
first, status-following automations behave as on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…istory parser execute.py wrote its run states as string literals, and RUN_STATE_NAMES - the list the History view uses to tell "Warn: ... [Exporting]" from a warning that merely ends in brackets - repeated them by hand, so a new or renamed state in execute_plan() would silently read as no state in History during a warning. - const.py defines each run state once (RUN_STATE_*); execute.py assigns and compares those, CHARGE/EXPORT_STATE_PRECEDENCE and RUN_STATE_NAMES are built from them. The strings are unchanged. - A test reads back every RUN_STATE_* constant, and the qualified forms execute.py builds, from behind a warning, and fails if one is missing from RUN_STATE_NAMES (checked by dropping Calibration from the list). - what-does-predbat-do.md lists the "Warn: ... [state]" form with the other status values. - The tariff comparison test restores previous_status and stubs call_notify, which its clean-run case changed on the shared fixture; two TEST_REGISTRY descriptions still described the old comma format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tate attribute predbat.status reads "Warn: ... [Exporting]" while a warning is active, so anything following Predbat's state had to parse it out of the text. The sensor now also carries the state the last completed run carried out, with no warning text, as its run_state attribute - "Exporting", "Demand, Hold for car". It is set when a run completes (clean, under a warning, or with a component error) and kept until the next one does, so a warning part way through a run or a run that stops before executing leaves it on the state the inverter is still following. It is empty after a restart until the first run finishes. The History view still decodes the state from the sensor's text, as some recorder databases don't keep attribute history; this is for live readers such as automations and the gateway. Tests that run record_final_run_status() now restore status_run_state on the shared fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
approved these changes
Oct 18, 2026
springfall2008
left a comment
Owner
There was a problem hiding this comment.
Reviewed at d09f8b3: no issues found.
- History reads the run's state back from
Warn: ... [<state>]. It only accepts a balanced trailing bracket that starts with a known run state, and the 255-character clamp keeps that suffix. execute.pytakes its run state names fromconst.py, the same constants the History parser checks against. A test fails if the two drift apart.predbat.statusgains arun_stateattribute for live readers. History still decodes the state from the text, as some recorder databases don't keep attribute history.- CI passes, and the branch merges cleanly with main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Posted by Claude on behalf of @chalfontchubby.
Summary
The History view can show the wrong state for a whole day when a warning recurs every cycle. On one install, a car integration logged out, so
car_charging_socreadunavailableand every cycle raisedWarn: Return bad float value unavailable from car_charging_soc. The battery force-exported from 16:00 to 19:00 exactly as planned, but History showed HoldChrg from 07:30 to 19:30.After this change, History shows the state Predbat actually carried out, and marks any slot that ran alongside a warning or error with 🚩. Hovering over the 🚩 names the problem.
Cause
record_status(..., had_errors=True)makes the warning the state ofpredbat.statusfor the rest of the run. Whenhad_errorsis set,record_final_run_status()only logs the run's real state. So a warning that recurs every cycle removes that state from the sensor, and from its history, for as long as the warning lasts.calculate_yesterday()rebuilds past slots from that history by substring."car_charging_soc"contains"charging", so every slot became a charge window, with its target set to the SoC actually reached. That renders as HoldChrg.Changes
Warn:/Error:status with no state recorded maps to an empty state. TheError: Complete run status X with component errorssummary is unwrapped toX.predbat.statusset to<warning> [<state>], for exampleWarn: ... [Exporting].startswith('Warn')orstartswith('Error')behave exactly as before./api/history/period(queried withoutsignificant_changes_only=0) drops attribute-only changes, so arun_statusattribute would be lost from history.debugis kept,error_countis not incremented again, and no notification is sent.Demand [Freeze exporting],Charging [Alert]).RUN_STATE_NAMESinconst.py), because warning text can end in brackets that are not a state, such as an interpolated list.status_warningreason after the slot's own reason.[state]suffix is never cut off.record_final_run_status()and the History parser share the component-error summary's wording throughconst.py.[<state>]suffix. Without this, a single recurring warning would change the sensor's state twice every cycle (bare warning, then with its state).to:still fires every cycle on the attribute updates, also as on main.Warn - ...is recognised as a warning too.predbat.statusalso carries the state the last completed run carried out as itsrun_stateattribute (Exporting,Demand, Hold for car), with no warning text, for automations and the gateway.output-data.mdand the status list inwhat-does-predbat-do.mddescribe theWarn: ... [<state>]form;output-data.mdalso lists therun_stateattribute.Behaviour changes to be aware of
predbat.statusreadsWarn: ... [Exporting]instead of justWarn: ....Warn/Error, is unaffected.== 'Warn: ...') will no longer match.Warn: ... No entity_id for charge_start_time [Charging]). The 🚩 and its reason make this visible.inverter.pysplitspredbat.statuson the first comma. It still skips cycles that raised a warning, as on main.predbat.statusgains arun_stateattribute. Automations should preferstate_attr('predbat.status', 'run_state')to matching the status text.execute.pynow takes its run state names fromconst.py(RUN_STATE_*), the same constantsRUN_STATE_NAMESis built from, so the History parser can't drift from what is written. The strings themselves are unchanged.Left as-is (for discussion)
record_status()andrecord_status_under_warning()share a lock, because component threads such as GE Cloud call them alongside the main loop.update_pred()clears the run's warning state under it too.record_final_run_status()'s choice between the two, which readshad_errorsoutside it. A component warning that lands between that read and the write is overwritten until the next run, as on main.A component thread can set
had_errorsvianon_fatal_error_occurred()without recording a warning. In that casepredbat.statuskeeps the last clean state, as on main. The comments incomponent_base.pyandsolis.pyrely on this. Changing it would put an error on the status for every cycle with a transient cloud-API failure, so I've left it alone.Across a gap in the recorder, a warning is held along with the state it arrived with, because both come from the same forward-filled series. The 🚩 therefore always describes the status the cell was rebuilt from.
Tests
[Demand [Freeze exporting]],[Charging [Alert]]). Warnings ending in their own brackets, with and without a state after them. Unbalanced brackets. The component-error summary. The Predheat form, bare and with a state.record_status_under_warning: the state recorded after the warning, keptdebug, no double-countederror_count, and the run's detail shown with its state. A repeated mid-run warning keeps its suffix and detail. A different error is shown bare. A component-onlyhad_errorsleaves the sensor alone.RUN_STATE_*constant, and the qualified formsexecute.pybuilds (Demand (Holiday),Demand [Freeze exporting],Charging, Hold for iBoost...), reads back from behind a warning. The test fails if one is missing fromRUN_STATE_NAMES, which I checked by droppingCalibrationfrom the list.record_status_run_state: the attribute before the first run, after a warned run, through a repeated mid-run warning, after a run that stopped early, after a clean run and after a component error.[state]suffix stays within 255 characters, both at the end of the run and when the warning repeats mid-run.record_status_concurrent: 40 warnings from 4 threads are all counted, and the notification is sent with the lock released. The component health test also puts the real component-error summary through the History parser.record_status_reset_locked: the start-of-run reset waits for the status lock.record_status_after_comparison: a warning the tariff comparison records ends up with the run's state and detail after it../run_all --quickpasses, and pre-commit passes on the changed files.🤖 Generated with Claude Code