Skip to content

Commit 8155d23

Browse files
fix(output): keep the state in front of a warning that repeats mid-run
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>
1 parent a4efa62 commit 8155d23

5 files changed

Lines changed: 38 additions & 3 deletions

File tree

‎apps/predbat/output.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def split_status_warning(status):
8585
component_suffix = " with component errors"
8686
if lowered.startswith(component_prefix) and component_suffix in lowered:
8787
return status[len(component_prefix) : lowered.index(component_suffix)].strip(), status
88-
if lowered.startswith("warn:") or lowered.startswith("error:"):
88+
if lowered.startswith("warn:") or lowered.startswith("warn -") or lowered.startswith("error:"):
8989
return "", status
9090
for marker in (", warn:", ", error:"):
9191
index = lowered.find(marker)
@@ -2732,7 +2732,16 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra
27322732
if not extra:
27332733
extra = ""
27342734

2735-
self.current_status = message + extra
2735+
# A warning that repeats the one already shown behind the previous run's state keeps that
2736+
# state in front of it, rather than flipping the sensor back to the bare warning for the few
2737+
# seconds until this run's own state is put back - two state changes every cycle for as long
2738+
# as the warning recurs. Any different warning or error, including the ones a run records
2739+
# when it bails out without executing, is shown bare.
2740+
state_message = message
2741+
if had_errors and self.current_status and self.current_status != message and self.current_status.endswith(", " + message):
2742+
state_message = self.current_status
2743+
2744+
self.current_status = state_message + extra
27362745
if notify and self.previous_status != message and self.set_status_notify:
27372746
if self.had_errors and had_errors:
27382747
# Already in error state, do not notify second error in a single run (spam)
@@ -2758,7 +2767,7 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra
27582767
# exactly the cycles the warning matters.
27592768
self.dashboard_item(
27602769
self.prefix + ".status",
2761-
state=message[:255],
2770+
state=state_message[:255],
27622771
attributes={
27632772
"friendly_name": "Status",
27642773
"detail": extra,

‎apps/predbat/tests/test_calculate_yesterday.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,6 +2186,7 @@ def _test_yesterday_status_core(my_predbat, failed):
21862186
("Error: Complete run status Hold charging with component errors: fox", "Hold charging"),
21872187
("Exporting, Warn: Return bad float value unavailable from car_charging_soc", "Exporting"),
21882188
("Freeze exporting, Hold for car, Error: Inverter 0 unable to read Export window", "Freeze exporting, Hold for car"),
2189+
("Warn - Unable to fetch data from sensor.outdoor_temperature", ""),
21892190
]
21902191
for status, expected in cases:
21912192
result = yesterday_status_core(status)
@@ -2291,6 +2292,12 @@ def _test_warning_status_is_not_a_charge_state(my_predbat, failed):
22912292
print("ERROR: the warning should be passed on to flag the slots, got {}".format(captured.get("status_warnings") and set(captured["status_warnings"].values())))
22922293
failed = True
22932294

2295+
# The real sequence in a cycle: the bare warning mid-run, then the state put in front of it
2296+
captured = _reconstruct_windows_from_status(my_predbat, lambda stamp: [(0, warning), (8, "Exporting, " + warning)])
2297+
if captured.get("charge_window_best") or not captured.get("export_window_best"):
2298+
print("ERROR: a bare warning followed by 'Exporting, Warn: ...' should rebuild exports only, got {} charge and {} export windows".format(len(captured.get("charge_window_best") or []), len(captured.get("export_window_best") or [])))
2299+
failed = True
2300+
22942301
# A freeze export under a warning stays a freeze - the exact state survives, not just its side
22952302
captured = _reconstruct_windows_from_status(my_predbat, lambda stamp: [(0, "Freeze exporting, " + warning)])
22962303
if not captured.get("export_limits_best") or any(export_mode_of(limit) != EXPORT_MODE_FREEZE for limit in captured["export_limits_best"]):

‎apps/predbat/tests/test_component_health_status.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ def test_record_status_under_warning(my_predbat):
140140
else:
141141
print("OK: state recorded in front of the warning, debug and error_count kept")
142142

143+
# The next run raises the same warning mid-run: the state stays in front of it, so the sensor
144+
# doesn't flip to the bare warning and back every cycle
145+
my_predbat.had_errors = False
146+
my_predbat.status_warning = None
147+
my_predbat.record_status(warning, had_errors=True)
148+
if my_predbat.dashboard_values[status_entity]["state"] != "Exporting, " + warning:
149+
print("ERROR: a repeated warning should keep the state in front of it, got {!r}".format(my_predbat.dashboard_values[status_entity]["state"]))
150+
failed = 1
151+
152+
# A different warning or error - e.g. a run bailing out without executing - is shown bare
153+
my_predbat.record_status("Error: Failed to fetch inverter data, not able to execute the plan", had_errors=True)
154+
if my_predbat.dashboard_values[status_entity]["state"] != "Error: Failed to fetch inverter data, not able to execute the plan":
155+
print("ERROR: a different error should be shown bare, got {!r}".format(my_predbat.dashboard_values[status_entity]["state"]))
156+
failed = 1
157+
143158
# had_errors set by a component thread with no warning recorded, right after a restart
144159
my_predbat.dashboard_values[status_entity] = {"state": "Demand", "attributes": {}}
145160
my_predbat.current_status = None

‎docs/output-data.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ If you toggle this switch in Home Assistant it will force Predbat to perform an
269269
270270
- **predbat.status** - Gives the current status & errors and logs any changes that Predbat makes to your inverter.
271271
The different Predbat status values and their meanings are detailed in [what does Predbat do](what-does-predbat-do.md#predbat-status).
272+
When a warning is reported during a plan update, the update ends by putting the state it carried out in front of the warning, e.g. `Exporting, Warn: ...`.
273+
An update that stopped before carrying anything out shows the warning or error on its own.
272274

273275
![image](https://github.com/springfall2008/batpred/assets/48591903/e24914b8-93d9-4217-812a-ac25a569a52c)
274276

‎docs/web-interface.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ The starting battery SoC for this simulation is carried forward from the end of
7777
'without Predbat' universe remains self-consistent — i.e. if Predbat had not been running, the battery may have had a different SoC at midnight than it actually did.
7878
This means the starting SoC shown in the 'Yesterday without Predbat' view can differ significantly from the 'History' view, which reflects what actually happened.
7979

80+
A time slot in the 'History' view where Predbat reported a warning or error is marked with 🚩 next to its state - the state shown is still the one Predbat carried out, and hovering over it names the problem.
81+
8082
The 'History' view also shows a **Debug** column with a download link on any time slot that has an automatically-captured [debug history snapshot](customisation.md#debug-history) from around that time - useful for grabbing the real Predbat state from the moment something looked wrong, without needing to have had debug mode switched on in advance.
8183

8284
You can easily change Predbat's planned activity for a slot by clicking on the slot time, then selecting Manual Demand, Manual Charge, Manual Export, Manual Freeze Charge or Manual Freeze Export to set the activity.

0 commit comments

Comments
 (0)