Skip to content

fix(control): the hourly import ceiling reports import_limit, not fuse_limit - #1783

Merged
frahlg merged 3 commits into
masterfrom
fix/import-limit-reason
Oct 21, 2026
Merged

frahlg merged 3 commits into
masterfrom
fix/import-limit-reason

Conversation

@frahlg

@frahlg frahlg commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Step 5 of the fuse rule in Fredrik's review of 21 October 2026: the hourly import ceiling is its own limit, named import_limit, never fuse_limit. The second of three PRs, independent of #1782 (direction of phase current); the third makes fuse protection one function.

Problem

Core folded the clock hour's import ceiling (peak_import_ceiling_w) into the EV cap and the battery allocator under the fuse's name:

  • a car held back by the hour's budget got commanded_reason: fuse_limit;
  • a battery boost the allocator stopped said "Fuse protection stopped boost" (fuse_safety_block);
  • the web page said "Limited by site power limits", and the web app and iOS say "Limited by the main fuse" for fuse_limit.

Near the end of a heavy hour the ceiling falls toward 0 while the phases are low, so the UI blamed the main fuse at the wrong time. This is the most likely cause of last night's case on an Easee Cloud + SolarEdge site (car idle overnight, goal_at_risk at 04:25, UI blamed the main fuse with low phases).

Change

  • EVFuseBudget records which limit left less room at each site reading (evMeterLimit.byImport); EVLimitReason names it.
  • The loadpoint reads it once per tick with the cap (SetFuseLimitReason, wired in main.go and the site simulation) and labels a capped command import_limit or fuse_limit. Manual-hold status and control feedback treat import_limit as a limit, like fuse_limit.
  • The allocator sets FuseSaturatedByImport when the hour's limit is the ceiling it hit. The boost then stops with import_limit_block.
  • Web: fuse_limit now means the main fuse ("Limited by the main fuse"); import_limit gets "Limited by the hourly import limit" and its own sentences in the EV card and boost list.

Not a registry name: commanded_reason and the feedback reasons are not in contract/registry.yaml, so there is no contract pair. Old clients show unknown reasons as "The box reports “import_limit”, which this version cannot describe." Follow-ups: copy web/control-feedback.js into ftw-webapp's src/vendor/ftw/, add import_limit to ftw-webapp src/lib/format/ev.ts, and add it to the iOS ControlFeedback.swift and EV.swift.

Deleted: nothing of size; this PR splits a label. It adds no settings.

Overlap: #1537 (draft) edits the status list in api_control_feedback.go and the title table in web/control-feedback.js. import_limit sits on its own lines, so neither PR rewrites the other's lines.

Site simulation, before → after

New row: end of a heavy hour, low phases. Night from 02:00, 6 kW hourly ceiling, house at 10 A per phase until 02:50, then 1.5 A, Charge now, Core restart at 02:09. The plant's own hour model expects no start in an hour's last quarter, as Core does.

master 6aa082f this PR
02:57, phases 1.5 A, car idle reason fuse_limit (fails) reason import_limit
03:00, new hour (not reached) car draws after 16 s idle with room
peak phase / over 24 A 10.0 A / 0 s 10.0 A / 0 s

All other rows unchanged.

Review fix (Codex P2, second commit): after a quiet part of the hour its limits can sit above what the fuse allows, while the ceiling's own start test still keeps an idle car off. The idle start now names the limit by that test. TestImportCeilingStartTestSaysImportLimit (40 quiet minutes, then an 8 kW house under a 7.5 kW ceiling) said fuse_limit before the fix.

Tests: TestImportCeilingSaysImportLimitNotFuse (the #1674 site: hour's ceiling → import_limit; fuse → fuse_limit), TestJointFuseAllocatorNamesTheHoursImportLimit, TestCommandedReasonImportLimit (cap 0 and 6,210 W over a hold), web tests for the new words.

Evidence

  • go test -p 2 ./internal/control ./internal/loadpoint ./internal/api ./cmd/ftw: pass
  • npm test: 868 pass
  • make verify (with GOFLAGS=-p=2): pass

The web copy changes need a person to look at them in a browser; I have not rendered them. Real-hardware evidence is still needed: a site with peak_import_ceiling_w set, at the end of a heavy hour.

🤖 Generated with Claude Code

…e_limit

Core folded the clock hour's import ceiling into the EV cap and the
battery allocator under the fuse's name. A car held back by the hour's
budget said fuse_limit, a boost it stopped said "Fuse protection", and
the web page and apps blamed the main fuse while the phases were low.

EVFuseBudget now records which limit left less room at each site
reading, and EVLimitReason names it. The loadpoint reads it once per
tick with the cap (SetFuseLimitReason) and labels a capped command
import_limit or fuse_limit. The allocator marks FuseSaturatedByImport
when the hour's limit is the ceiling it hit, and the boost stops with
import_limit_block. fuse_limit now means the main fuse alone; the web
page says so and gives the hour's limit its own words.

The site simulation's new row is a night with a 6 kW hourly ceiling, a
heavy first 50 minutes, a Core restart at 02:09 and phases at 1.5 A
near the end of the hour. On master the idle car says fuse_limit; now
it says import_limit and starts within 16 s of the new hour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T14:50:30.742272Z cb55a26 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb55a26b9c

ℹ️ 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".

}
step := evPhaseMarginA * s.SiteFuseVoltage * pool.phases
// The label follows whichever limit leaves less room.
lim.byImport = math.Min(r.limitW, r.riseW) < r.fuseW

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for the idle-start ceiling when naming the limit

When an idle car plugs in after a low-import part of the hour, limitW and riseW can both exceed the fuse ceiling, while the separate start ceiling still blocks charging. For example, with a 7.5 kW import setting, ample accumulated hourly slack, an 8 kW house load and a 25 A three-phase fuse, this comparison sets byImport to false. However, stepEVIdleStart rejects the start because startLevelW is capped at PeakImportCeilingW, leaving negative import headroom despite sufficient fuse headroom. The updated UI consequently says “Paused by the main fuse” when the import policy is responsible. Include the binding idle-start constraint when selecting the reason.

Useful? React with 👍 / 👎.

…imit

Review of #1783 (Codex): after a quiet part of the hour, the hour's limits
can sit above what the fuse allows, so the label compared them and said
fuse_limit. An idle car is then kept off by the ceiling's own start test,
which caps the start level at the ceiling itself. Under a measured hour
the idle start now names the limit by that test: the start level against
this reading, and import_limit when the dwell's mean import fails it.

TestImportCeilingStartTestSaysImportLimit: 40 quiet minutes under a
7.5 kW ceiling, then an 8 kW house and a car plugged in. It said
fuse_limit; it says import_limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1783: the field case as a test. A 7.5 kW hourly ceiling, a
25 A fuse, a light first half hour and a car plugged in at 22:40 beside
6 kW, and 8 kW, of house load. Every tick to 23:00 is checked; on
cb55a26 it said fuse_limit from 22:40:01, with 1a79330 it says
import_limit throughout.

The site simulation's last-quarter rule now states its 15 minutes
instead of copying importHourRiseMinLeft, so a wrong constant shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit b5702e7 into master Oct 21, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant