* feat(web): plot what the plan predicted against what the battery actually did
The Battery chart shows four predicted SoC traces and one measured line, but they never meet: every
series except Actual is the future, and Actual is the past. soc_kw_best's results/today attributes
are rewritten each cycle, so by the time a predicted moment arrives the prediction for it is gone and
there is nothing left to score the plan against.
soc_kw_best now also carries soc_h1 and soc_h8 - what this plan expects the battery to hold one and
eight hours out - as plain attributes, which Home Assistant keeps in history. The chart reads them
back with a matching time offset, so each lands on the moment it was describing and sits directly
alongside the measured SoC as "Predicted (+1h)" and "Predicted (+8h)".
This is the pattern the LoadML chart already uses for load, applied to the battery.
The offset is the part that has to be right: a value recorded at 10:00 describing 11:00 must be
plotted at 11:00, or a correct plan reads as wrong by exactly the horizon. Both the recording and the
offset are pinned by tests, the latter verified by removing the shift and watching the assertion
catch it.
Nothing is added to the plan or the prediction - the values are already computed, they were simply
not being kept. A plan too short to reach a horizon falls back to the final SoC rather than
publishing None, which would break the attribute and take the series with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(web): separate car charging from house load on the LoadMLPower chart
The Actual series is whatever the inverter reports as house load, which on a charger inside the CT
clamp includes the car - a 7kW spike every session. The ML forecast it is plotted against has car
charging subtracted out (car_charging_hold in load_ml_component), so the two are not measuring the
same thing and every charging session reads as a large forecast miss the model was never attempting.
Two series are added rather than one. Car Charging Power shows the draw on its own, because it is
real and worth seeing. Load Power (Actual, less car) is the like-for-like comparison against the
forecast. Keeping both means nothing is hidden by the correction: the total is still there, the
house-only figure is there beside it, and the difference between them explains itself.
The subtraction is clamped at zero. The two readings come from different sensors on their own
cadences, so the car can momentarily exceed the load reading, and that has to read as no house load
rather than as negative power.
Both series are absent when no charger is configured, in which case the chart is exactly as it was -
car_charging_power is only published when a charger sensor exists, which is how its absence
distinguishes "no charger" from "charger idle".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(docs): record the entities the new charts read, and scan both history readers
The Battery and LoadMLPower charts now read the history of predbat.soc_kw_best and
predbat.car_charging_power, but the FAQ's recorder example excludes the whole predbat domain and
lists what to keep - so anyone following it would have got two permanently empty series and no
indication why.
test_faq_recorder_config caught car_charging_power and missed soc_kw_best, because it scanned only
get_history_wrapper() calls. get_history_with_now_attrs() wraps that function but is written as its
own call, so every chart using the wrapper was invisible to the check. The pattern now matches both,
which is what turns the second omission from something a reader has to notice into something the
suite reports.
Verified by removing the new soc_kw_best line and confirming the widened scan reports it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(web): match the car series on time, not on identical timestamps
Load Power (Actual) and Load Power (Actual, less car) drew as two identical lines. The subtraction
looked up the car value by exact timestamp, but the two series come from different entities that Home
Assistant records independently, and prune_today() keys each result on its own source timestamp. The
keys therefore almost never matched, .get(stamp, 0) returned zero every time and nothing was
subtracted - silently, because a subtraction of zero looks like a car that is not charging.
Each base point now takes the nearest car sample in time, within one publish cycle. Nearest rather
than most-recent-at-or-before because both are published in the same cycle, so the offset between
them is jitter that falls either way; pairing only with the earlier sample would use the previous
cycle's car value whenever it fell the wrong way, which is most visible the moment the car stops and
a stale reading would wipe out the whole house figure. Bounded, because a car sensor that dropped out
hours ago must stop being subtracted rather than blanking the house figure for as long as it is away.
The original test used identical keys for both series, so it exercised the arithmetic and never the
alignment - it passed throughout. It now offsets the timestamps the way production does, and fails
against the old lookup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>