Repository navigation
fix(history): keep Home use through device dropouts and restarts - #1753
Conversation
…me use Since #1741 a Core restart (18-20 s), a source unknown for a few seconds or a writer pause over 15 s made the whole day's load_wh null, and an unknown PV also nulled pv_wh. The source writer now holds an episode that starts at the last fully known snapshot. If every source is known again with the same topology within LoadEstimateRecoveryWindow (2 min), the episode is written as an estimate marked EstimatedLoad: measured sources keep their snapshots, unknown ones hold the anchor's values. Otherwise it is written exactly as before. Pending time stays out of totals and expected coverage, and the day waits for it before closing. On open Core marks the cursor as stopped instead of deleting it, so a brief stop is bridged as an estimate and a longer one starts a new period as before. Older Cores still delete the cursor on open. Relates to #1750. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Short source gaps may become estimated history once #1753 lands, and then a training label. Keep these tests about gaps that stay unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows Fredrik's two-layer direction (19 October). Display is forgiving: an unknown PV, battery or EV source holds its last known value; the site meter, a writer pause or a restart holds for at most two minutes; a topology change no longer stops integration. A day's flow is unknown only when it is unknown or unrecorded for more than a tenth of the range. Only two fully measured snapshots a safe tick apart, of one topology and one process, make a measured interval. Every other interval keeps EstimatedLoad, and quarter-hour retention now keeps that bit, so the training layer can skip it. Replaces the pending episodes of the first version. Closed source-era days cached under the old rule are recomputed once. Relates to #1750. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A held value that turns the household residual negative cannot be right: the source changed while unknown, such as a charger that finished. That interval leaves the held flows unknown instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased on the rewritten #1753, whose writer marks an interval as an estimate at an unknown source, a stop, a long gap or a topology change. A night past civil dusk therefore trains only if every snapshot is fully known and the sleeping inverter keeps its device in the topology. The devices line moves into historyDevices so the test drives the same code as persistTelemetryTick. The test runs eight hours of a sleeping inverter through it, with and without a map pin. TestDarkPVByTimeOfDay now also checks that PV reads 0 W and that Load stays unknown for any cause but PV; it replaces the two-hour series test, whose day-level outcome the new writer decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
historySources set MissingLoad for every reason except invalid_balance. Telemetry puts a source's own failure there too, such as "offline:sma:pv" or "stale:easee:ev", so a real snapshot with a silent inverter or charger voided the whole balance and the source writer held nothing. Only configuration and duplicate-flow reasons now set MissingLoad; a failed source is unknown on its own flag. Found by the #1755 session through historySources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased on the rewritten #1753, whose writer marks an interval as an estimate at an unknown source, a stop, a long gap or a topology change. A night past civil dusk therefore trains only if every snapshot is fully known and the sleeping inverter keeps its device in the topology. The devices line moves into historyDevices so the test drives the same code as persistTelemetryTick. The test runs eight hours of a sleeping inverter through it, with and without a map pin. TestDarkPVByTimeOfDay now also checks that PV reads 0 W and that Load stays unknown for any cause but PV; it replaces the two-hour series test, whose day-level outcome the new writer decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased on the rewritten #1753, whose writer marks an interval as an estimate at an unknown source, a stop, a long gap or a topology change. A night past civil dusk therefore trains only if every snapshot is fully known and the sleeping inverter keeps its device in the topology. The devices line moves into historyDevices so the test drives the same code as persistTelemetryTick. The test runs eight hours of a sleeping inverter through it, with and without a map pin. TestDarkPVByTimeOfDay now also checks that PV reads 0 W and that Load stays unknown for any cause but PV; it replaces the two-hour series test, whose day-level outcome the new writer decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1753 (85100d6) no longer marks Load unknown when a single source fails, so darkPV need not clear it. It now changes only the PV flow and its marker; a configuration fault keeps Load unknown on its own. The night test without a pin checks Home use again: the writer holds PV's last value as an estimate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(forecast): train load models on closed history intervals Core computed household load three ways: history balanced per-source energy per interval, the Energyplan tracker kept its own quarter-hour accumulator, and the built-in load model learned from one instant reading per minute. The models learned a different quantity from the Home use shown in History. Add state.HouseholdIntervals, which returns closed, valid intervals from the same source energy as daily totals. The built-in model trains once per closed quarter on its mean, and the tracker takes its load labels from the same quarters. PV keeps its accumulator for the curtailment flag and waits until history decides the quarter, so each immutable observation is written once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style: gofmt the tracker observation test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: use gaps longer than the history bridge window Short source gaps may become estimated history once #1753 lands, and then a training label. Keep these tests about gaps that stay unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loadmodel): preserve stored minute-based heating bounds * fix(forecast): train only on fully measured household intervals History may show Home use across a bridged or estimated gap, but a load model must not learn from it. Leave out intervals with EstimatedLoad or BalanceMismatch as well as a missing source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85100d65aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if reading.Reason != "" && reading.Reason != "invalid_balance" { | ||
| // sources. Missing configuration and duplicate electrical flows do. A | ||
| // failed source is unknown on its own flag, so history may hold it. | ||
| if reading.Reason != "" && reading.Reason != "invalid_balance" && !telemetry.SourceFailure(reading.Reason) { |
There was a problem hiding this comment.
Preserve healthy peers when one source fails
When multiple devices share a DER type, a failure of one device makes ForecastReading.Known[kind] false for the entire type while the aggregate wattage still contains the healthy peers. Treating that reason as holdable then causes the loop below to zero the aggregate and holdSourceIntervalTx to reuse the last aggregate value. For example, if one of two PV inverters becomes stale while the other changes output, Home use is calculated using both inverters' old combined output, potentially indefinitely, rather than holding only the failed inverter and retaining the healthy one's current reading. The persisted representation must preserve per-source values or otherwise separate the failed contribution before enabling this bridge.
Useful? React with 👍 / 👎.
Rebased on the rewritten #1753, whose writer marks an interval as an estimate at an unknown source, a stop, a long gap or a topology change. A night past civil dusk therefore trains only if every snapshot is fully known and the sleeping inverter keeps its device in the topology. The devices line moves into historyDevices so the test drives the same code as persistTelemetryTick. The test runs eight hours of a sleeping inverter through it, with and without a map pin. TestDarkPVByTimeOfDay now also checks that PV reads 0 W and that Load stays unknown for any cause but PV; it replaces the two-hour series test, whose day-level outcome the new writer decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1753 (85100d6) no longer marks Load unknown when a single source fails, so darkPV need not clear it. It now changes only the PV flow and its marker; a configuration fault keeps Load unknown on its own. The night test without a pin checks Home use again: the writer holds PV's last value as an estimate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(history): count unknown PV as 0 W after sunset A PV inverter that sleeps at night is the most common remaining reason for a day without Home use. Göran's SMA was silent for 145 s at 02:22 on 19 October with 0 W on both sides, longer than the 2-minute bridge. While the sun is below the horizon at the site's map pin, Core now records an unknown PV source as 0 W, marked EstimatedLoad, never as a measurement. An inverter the watchdog took offline keeps its device in the history topology, so the night is not cut out. The source writer carries the marker into every interval an estimated snapshot bounds. Without a pin (unset, 0,0 or the untouched default) nothing changes. By day the gap stays unknown. Grid, battery and EV are not affected. Relates to #1750. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(history): count PV past civil dusk as measured 0 W Load models train only on fully measured intervals (#1754). Marking every dark PV snapshot as an estimate would drop each night with a sleeping inverter from training. Past civil dusk (zenith over 96°) no panel produces, so an unknown PV source now reads as a known 0 W without EstimatedLoad: the night counts in Home use and in training. Between sunset and civil dusk diffuse light remains, and the 0 W stays a marked estimate. No new bit records the physical zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(history): a full night with a sleeping inverter stays measured Rebased on the rewritten #1753, whose writer marks an interval as an estimate at an unknown source, a stop, a long gap or a topology change. A night past civil dusk therefore trains only if every snapshot is fully known and the sleeping inverter keeps its device in the topology. The devices line moves into historyDevices so the test drives the same code as persistTelemetryTick. The test runs eight hours of a sleeping inverter through it, with and without a map pin. TestDarkPVByTimeOfDay now also checks that PV reads 0 W and that Load stays unknown for any cause but PV; it replaces the two-hour series test, whose day-level outcome the new writer decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(history): let the dark rule touch only PV #1753 (85100d6) no longer marks Load unknown when a single source fails, so darkPV need not clear it. It now changes only the PV flow and its marker; a configuration fault keeps Load unknown on its own. The night test without a pin checks Home use again: the writer holds PV's last value as an estimate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(telemetry): name offline drivers through a health-gated accessor historyDevices read cached PV readings directly to find a sleeping inverter. Move the health check into telemetry.OfflineDrivers, which returns only driver names, so no caller handles a stale cached value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…1758) The negative-balance guard cleared only the interval's copy of a held value; the cursor kept it. While the source stayed unknown, a later rise in house use made the same rejected charge valid again, so Home use lost it and a phantom charge was recorded. The rejected sources now stay unknown in the cursor until they report again. Found by Codex's adversarial review of #1753. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Relates to #1750. This fixes the cause; #1751 shows a subtotal after the cause has struck. Reworked on 19 October after Fredrik's direction: Home use has a forgiving display layer and a strict training layer.
Problem
Since #1741 (first in v0.153.0-beta.1), one short disturbance makes the whole day's
load_whnull:aggregateDayEnergyORs each interval's missing bits into the day and needs full coverage. One unknown interval makes the whole day unknown; an unknown PV also makespv_whnull.So every day with an update has unknown Home use for every beta tester. On Göran's box on 19 October, five 5-minute intervals failed: two Core updates (00:50, 13:32), SMA silent for 145 s at 02:22, and EV unknown around 12:38.
Two layers
Display is forgiving. Home use is the residual grid − PV − battery − EV, as well as the evidence allows.
Training is strict (#1754). It uses only minutes without
MissingPower,BalanceMismatchorEstimatedLoad.Contract between them. Only two fully measured snapshots a safe tick apart, of one topology and one process, make a measured interval. Every other interval carries
EstimatedLoadinhistory_quality. Readers never clear it, and retention now keeps it when it merges minutes into quarter hours (historyMissingAggregatehad dropped bit 256).Also:
historySourcessetMissingLoadfor every telemetry reason exceptinvalid_balance. Telemetry puts a source's own failure there too (offline:sma:pv,stale:easee:ev), so in a running Core a silent inverter voided the whole balance and the hold did nothing. Now only configuration and duplicate-flow reasons setMissingLoad(telemetry.SourceFailure). The state tests feed the writer only the source's own flag, so they did not see this;TestHistoryHoldsASilentSourceThroughTheBalancegoes throughbuildHistoryPointand fails without the fix (1010, PV and Load). Found by the fix(history): count unknown PV as 0 W after sunset #1755 session.source-day-coverage-v1). With the new day rule alone, rows the old writer already saved become known days again; see below.No new setting, table, API field or UI. None of #1751's files are touched.
Evidence
TestSourceLoadDayForgivesShortGaps: 2 h of a 1000 W house, 5 s snapshots, one disturbance one hour in. The first version of this PR is 604ed96.Every known day is
estimated. In the "EV starts" case the held charger value puts 2 kW × 15 s (8.3 Wh) into the house. "Meter unknown 5 min" loses the four whole minutes the unknown 185 s touch: Home use counts by the minute.Old writer, new day rule (rows as v0.153/v0.154 saved them): 20 s restart 1994.4 Wh, EV 15 s 1983.3, PV 145 s 1950, meter replaced 1998.6, meter 5 min 1900; EV 30 min stays null. So after updating, a day like 19 October should show Home use, a little low, once its cache is recomputed. Not proven on Göran's data.
14-day simulation (seed 42; daily restarts of 15–40 s, PV unknown 30–300 s, EV 10–20 s, battery 60–180 s, meter 20–90 s, a lagging meter, broken values and one 12-minute outage), run with #1754 (fe42b90):
The remaining 8 % on that day is the house's own use during the ~80 minutes the guard leaves unknown. Only the charger's energy counter could correct it afterwards.
Other new tests:
TestSourceLoadMarksEveryEstimatedMinute: an EV gap of 15 s marks exactly one minute, and the marker survives the quarter-hour merge.TestSourceDayCacheRecomputedOnce: a day cached under the old rule is dropped once, and a recomputed one stays.Existing tests changed on purpose. In
TestSourceLoadKeepsUnknownDistinctFromZero, "missing EV", "write gap" and "topology" are now estimates and must carry the marker; "partial tail" and "time reversal" stay unknown.TestSourceLoadUsesSnapshotTimeAndDoesNotBridgeRestartbecomes…MarksRestartBridge: a 10 s stop is bridged with the marker, a stop over 2 minutes is not.make verify: passed: all Go suites (internal/stateincluded), protocol, scripts, the Docker release context, vet and build. Nothing skipped.Not verified
history_sources.go. This rework replaces its two writer lines (its marker rule is included here), so it needs a rebase.For Fredrik to decide
coverage_msalready shows the gap. Draft fix(history): keep Home use when at most 1 % of the day is unknown #1756 proposes 1 % (14.4 minutes) for the same rule, with a 70-day simulation: 69 of 70 days known, mean error 0.039 kWh with fix(history): count unknown PV as 0 W after sunset #1755. With the hold in this PR, little unknown time is left, so either works for short gaps; they differ for outages between 15 minutes and 2.4 hours. One constant,maxSourceDayUnknownShare.LoadEstimateRecoveryWindow. A longer stop is unrecorded time, not a hold.🤖 Generated with Claude Code