Repository navigation
fix(forecast): train load models on closed history intervals - #1754
Merged
Merged
Conversation
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>
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>
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>
This was referenced Oct 19, 2026
frahlg
added a commit
that referenced
this pull request
Oct 19, 2026
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>
frahlg
added a commit
that referenced
this pull request
Oct 19, 2026
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>
frahlg
added a commit
that referenced
this pull request
Oct 19, 2026
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>
frahlg
marked this pull request as ready for review
October 20, 2026 04:33
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. |
frahlg
added a commit
that referenced
this pull request
Oct 20, 2026
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>
frahlg
added a commit
that referenced
this pull request
Oct 20, 2026
* 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>
frahlg
added a commit
that referenced
this pull request
Oct 20, 2026
History leaves an optional PV source out of Home use when it has never reported, as a hybrid may have no panels. The forecast requires it, and both load models now train on history intervals, so 2 kW of unseen PV could become a 1 kW label for a 3 kW house (Codex review of #1754). History options now carry the optional flows. While one has no live reading, the snapshot is marked EstimatedLoad: Home use is unchanged and training skips it. Past civil dusk the PV is 0 W by physics, so the night still trains. Forecast revisions are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
Core worked out household load three ways:
history_source_energy)ForecastAccumulator: 15-minute trapezoid means of aligned instant readings, any invalid instant drops the quarterloadmodel,mpc.Load)HouseholdWreading per minuteSo the models learned a different quantity from the Home use people see, and the built-in model learned from single instant readings. Fredrik asked for one common basis.
Two layers
Fredrik set the direction on 19 October: Home use has two layers.
The contract between them is the
history_qualitymask. Anything the display layer bridges or estimates carriesEstimatedLoad; training leaves out every interval withMissingPower,BalanceMismatchorEstimatedLoad. PV at 0 W while the sun is well below the horizon is physics, not an estimate, so a PV inverter asleep all night must not remove the night from training (being settled in #1755).Change
state.HouseholdIntervalsreturns closed, fully measured intervals from the same source energy as daily totals. An interval is closed once committed source progress passes its end, so a returned value never changes. Intervals with a missing source, a recording gap, an estimate or a negative balance are left out. It also returns where the closed range ends, so callers move past intervals left out.loadmodel.Servicetrains once per closed quarter-hour on its mean instead of one instant reading per minute. The model already averages within a day per weekly hour, so the hourly means keep their meaning. Cold-day counters keep the minute units used by stored models: each new quarter adds 15 minute units to the count and both sums. This preserves partial-day evidence and the six-hour bound without resetting learning. It persists after each trained quarter.Quality: source_interval_balance_v1) and drops its household accumulator. PV keeps its accumulator, since PV labels need the curtailment flag. Observations are immutable per quarter, so PV waits until history decides the quarter (at most 2 minutes), and each quarter is archived once.Model state, feature hash and learning revisions are unchanged, so no site loses learning.
This follows docs/roadmap.md: household Load history balances source energy over the same interval, and every use is checked against the same stored evidence.
Relation to other work
Evidence
go test ./internal/state ./internal/loadmodel ./cmd/ftw ./internal/api ./internal/telemetry: all pass.TestHouseholdIntervalsMatchDailyTotal: the sum of training intervals equalsDailyEnergy.LoadWh.TestHouseholdIntervalsLeaveOutIncompleteEvidence: missing EV, a 3-minute recording gap and a negative balance give no label; neighbours keep theirs.TestHouseholdIntervalsWaitForCommittedProgress: an open quarter is never returned.TestTrainsOnceOnEachClosedValidHouseholdInterval,TestLearningCutoffSkipsIntervalsStartedBeforeIt,TestIntervalReadInFlightIsDiscardedfor the built-in model.TestForecastPVWaitsForTheHouseholdDecision, reworkedTestForecastConfigChangeBreaksIntervalandTestForecastIndependentPVIntervalsAndCurtailmentfor the tracker. Readings carry a wrong instant house value; labels must come from history.loadmodel; history now owns those validity rules and the state tests cover them.TestRestoredMinuteHeatingBoundsKeepLearning: restores both learned profiles and the active profile unchanged from minute-based state, persists and reloads them after a new quarter, rejects 30 old minutes as a full cold day, and accepts 345 old minutes plus one new quarter as exactly six hours. It fails before the fix and passes after it.go test -race ./internal/loadmodel: passed.make verify: passed (vet, tests, build).TestHouseholdIntervalsLeaveOutEstimatedMinutes: an estimated minute keeps its quarter out of training while daily Home use still shows it.Watch after the beta:
loadmodelsample counts grow per quarter, and the Energyplan learning status stays healthy.🤖 Generated with Claude Code