Repository navigation
fix(control): the import ceiling caps each clock hour's mean import (#1674) - #1699
Conversation
…1674) The import ceiling acted every second. Under a 7.5 kW ceiling a short house peak cut the car, and the 23:45 case held at 5,520 W. Swedish power tariffs bill the mean import over each clock hour, the hours the planner's demand charges use. The control tick now adds the site meter's import to the local clock hour. The hour's limit is the budget left over the time left; the EV guard's cut, the battery fuse guard and the joint allocator use it. The fuse stays an instant limit. Increases reach only a slower level: whole steps with one in hand, at most once a minute, five minutes after a cut and not in the hour's last quarter. A car starts only with room under the ceiling itself, so it never starts on slack only to stop at the next hour. A meter gap counts at no less than the hour's rate and never raises a limit. At boot the hour's import comes from state.db. /api/status shows the hour's budget left and the limit in force. Overlaps #1675. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
… car (#1674) Göran, 17 October 14:27:59 UTC: with the car at its 4,140 W minimum beside 3.3 kW of house load, one reading 24 W over the 7.5 kW ceiling stopped it, and the room left (4.2 kW) never met the minimum plus a step again. The EV guard now keeps the fuse's part apart from the ceiling's part. The fuse cuts on every reading. The ceiling cuts at once on an overage of a step or more, after a meter gap, or while the hour is not measured; a smaller overage must last 20 s. A restart judges the ceiling on the mean import over the 35 s wait and needs room for the minimum only, under the rise level and at most the ceiling. A ceiling set during the hour counts from then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
… gate The telemetry-read guard counted two direct reads of the cached meter in import_hour.go. Store.OnlineMeterW returns the site meter's power and arrival time only while its driver is still reporting, so a watchdog-offline meter's last value never counts as a new reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.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: 826d6f03c7
ℹ️ 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".
| start := control.ImportHourStart(now) | ||
| wh, covered, err := st.ImportWhIntervals(context.Background(), [][2]int64{{start.UnixMilli(), now.UnixMilli()}}) |
There was a problem hiding this comment.
Preserve a mid-hour ceiling baseline across restarts
When the ceiling is enabled or changed partway through an hour and Core restarts before that hour ends, this query restores import from the beginning of the hour. At runtime stepHourImportLimits intentionally discards import from before the new setting, so after the restart that earlier import is incorrectly charged against the new ceiling and can sharply reduce the limit or stop charging. Persist the activation/baseline time or otherwise seed only the portion accumulated since the current ceiling took effect.
Useful? React with 👍 / 👎.
Problem and result
#1674: the import ceiling (
peak_import_ceiling_w) acted every second. Under a 7.5 kW ceiling with 0.9 kW of house load, #1695 holds the car at 5,520 W with one step free, and a short house peak cuts it. Swedish power tariffs bill the mean import over each clock hour, and the planner already models demand charges per local clock hour (mpc/demand.go). The ceiling throttled more than the tariff needs. Fredrik agreed on 17 October that it should act on the hour's mean. The fuse stays an instant limit.Göran's support dump from 17 October (v0.147.2-beta.1, 14:26–14:29 UTC) shows the cost. The ceiling went on with the car at 10,350 W and about 3.3 kW of house load. The car came down to its 4,140 W minimum. One reading of 7,524 W, 24 W over the ceiling, then stopped it (
overage_w=24 cap_w=4116). After that the room was about 4.2 kW, short of the minimum plus a step (4,830 W), so it never restarted until he turned the ceiling off. On the hour's mean, 3.3 + 4.14 kW = 7.44 kW holds a 7.5 kW ceiling.go/internal/control/import_hour.gokeeps the clock hour's import and two limits.ObserveSiteImportruns every control tick (main.go, underctrlMu, before the loadpoint tick). It adds the positive grid power between readings, split at local clock hours in the box's zone, the zone the planner uses. It reads the meter throughStore.OnlineMeterW, a new health-gated accessor ingo/internal/telemetry, so a watchdog-offline meter's last value never counts as a new reading. No charger or battery reading takes part.effectiveImportCeilingW, so the battery fuse guard and the joint allocator use it, and the EV guard cuts on it. It is never above the fuse less its margin.evMeterRoom). The fuse still cuts on every reading. The ceiling cuts at once on an overage of a charging step or more, after a meter gap, or while the hour is not yet measured. A smaller overage must last 20 s first, so one reading over the ceiling changes nothing. The energy it lets through counts against the hour and lowers the limit at the next minute.ImportWhIntervals, the planner's source) and seeds it. Time the record does not cover counts at the ceiling./api/statusaddspeak_import_hour_left_whandpeak_import_limit_wbesidepeak_import_ceiling_w, null while it is off. No new setting; the tariff model has no window length.site meter over limitlog line now namesimport_limit_w.Near the hour's end the limit is the exact budget rate, so a cut takes the smallest step that keeps the hour inside it. The car stops only when even its minimum beside the house would overrun the hour.
One departure from the brief
The brief suggested taking a house-load reserve, such as the mean house load, off the limit. I did not. The EV guard's room is the limit less the measured import, and the measured import already holds the house. Taking the house off again counts it twice: at 23:45 the limit would be 7.5 − 0.9 = 6.6 kW against 7.11 kW measured, and the car would be cut. The rise rules do the reserve's work instead.
Scope and safety
ImportWhIntervalsmeans the hour so far counts at the ceiling, which is the old behaviour.ImportWhIntervals, counts at the ceiling. That is neutral, the old behaviour, but it understates an hour that ran above the ceiling. The legacy history path (state/cost.go) counts a NULLgrid_was covered 0 W; it is used only before the site-energy tables exist.patch, as the brief asked. The ceiling's meaning changes without any user action, sominoris arguable.evMeterRoomkeeps the fuse's part and the ceiling's part apart, andimportHourLimitsW()returns the ceiling's limits alone, for work that stacks on this (the fuse heat budget).web/index.html: the ceiling's hint now says it caps the hour's average..card-hintisdisplay: none, so nothing visible changes. Checked in a browser.ev_fuse_budget.go(evMeterRoomWbecomesevMeterRoom; the idle start moves tostepEVIdleStart),ev_battery_headroom.go(one line) and theStatestruct indispatch.go.Verification
No hardware run yet. Göran's site (segran2) is the natural first test, with the ceiling on. Everything below is simulated.
Simulated with the #1674 harness: an Easee-like 3-phase charger with 5 s command delay and 10 s late telemetry, a 25 A site and 1 s meter readings.
go/internal/control/import_hour_test.go:go test ./internal/control/ -race -count=3passes.make verifypasses (76 packages, vet and build clean), andnpm testpasses 760 of 760.Existing tests that set a ceiling without feeding meter readings keep the old flat limit, so the guard's own behaviour (#1695) is unchanged and its tests still pin it.
Checklist
Refs #1674.
🤖 Generated with Claude Code