Skip to content

fix(control): read a whole-ampere meter's phase current from its power - #1932

Open
frahlg wants to merge 1 commit into
masterfrom
whole-amp-phase-current
Open

frahlg wants to merge 1 commit into
masterfrom
whole-amp-phase-current

Conversation

@frahlg

@frahlg frahlg commented Oct 28, 2026

Copy link
Copy Markdown
Member

Problem

Many P1 meters report phase current in whole amperes. p1_dsmr and zuidwijk_p1 pass DSMR 1-0:31.7.0 straight through as l1_a. Core reads each phase's magnitude only from l*_a. A truncating meter hides up to 1 A, a rounding meter up to 0.5 A.

With a 0.5 A margin that stayed under the fuse rating. #1914 makes the margin 0 by default. The car then fills a phase until the meter reads the rating, and the real phase sits up to 1 A over In with no end: longer than the I²t rule allows.

A review of #1914 (on a5cdeae, the whole fuse stack) measured it in the site sim, with exact phase and total power and only the amps quantised. 16 A fuse, the car asks 16 A, last 20 of 30 min:

House per phase Meter Margin L1 Time over In
4.6/1/1 A truncates 0 16.60 A 20 min
4.6/1/1 A truncates 0.5 A 15.60 A 0 s
4.45/1/1 A rounds 0 16.45 A 20 min
4.45/1/1 A rounds 0.5 A 15.45 A 0 s

The same meters report each phase's power (l*_w) and voltage (l*_v). Both drivers emit them.

Change

go/internal/control/fuse_heat.go, phaseCurrentsA: a phase's current is at least its power over its measured voltage, since |P| ≤ V·I. This applies when:

  • the reading has l*_v for the phase, within 0.8 to 1.2 × the fuse's nominal voltage, and
  • the phase powers are present and add up to the reading's total (the same check that already gates the export sign).

The measured voltage matters. Dividing by the nominal 230 V reads high on a 240 V grid (16 A reads 16.7 A), and that acts as a hidden margin. A voltage outside the band is a driver or scale fault and bounds nothing. A meter without l*_v reads as before.

Everything that reads phaseCurrentsA gets the bound: the fuse heat model, the car's per-phase fuse line and its battery-yield check, and SitePhases for the one-phase car check. On master the battery's fuse relief (perPhaseOverageReadingW) and phaseImbalanceA still read l*_a alone. #1891 moves battery relief onto phaseCurrentsA, so it gets the bound once the fuse stack lands. This PR does not change phaseImbalanceA.

Tests

  • TestPhaseCurrentsReadWholeAmpMetersFromPhasePower: a meter that reads 16 A while its power shows 16.6 A at 230 V reads 16.6 A, as import and as export. On a 240 V grid, 16 A at 3840 W stays 16 A. No phase voltage, a voltage off scale (23 V), phase powers against the total, or a reactive load (power below V·I): each reads l*_a as before.
  • TestEVHardLimitCutsWhatAWholeAmpMeterHides: a whole-ampere meter at 26 A carrying 26.9 A gets the cut a meter showing 26.9 A gets. A 26.0 A phase on a 240 V grid gets the 26.0 A cut.
  • Before the fix (master's fuse_heat.go), both fail: "truncated 16.6 A: [16 1 1]" and "whole-ampere meter cap 8970.0 W, exact meter 8349.0 W". With the power divided by the nominal voltage instead, both fail on the 240 V case (16.70 A; cap 8190 W against 8970 W).
  • The same two tests fail without the fix and pass with it on the fuse stack too (fix(config): fuse safety_margin_a defaults to 0 #1914's head plus this commit, unpacked with git archive).
  • Site sim on that stack copy, with the review's amps-only quantiser and l*_v added to the sim meter (both local probes, not committed): L1 ends at 15.60 A (truncating) and 15.45 A (rounding) at margin 0, 0 s over In. The full control package passes there with l*_v on every sim reading, apart from the probe file.
  • go test -race -count=1 ./internal/control passes (159 s). make verify exits 0. gofmt is clean on the touched files.
  • Repo hygiene against origin/master: no planning docs; telemetry reads 134, none added; brand cleanup passes; both helper unit suites pass.

Risk

Low. The bound only raises a phase's current, and only to what the meter's own power and voltage prove. Fuse protection can only act sooner. A meter whose power and voltage disagree with its current (wrong scale, wrong phase order) could make Core cut more than it needs; the voltage band and the add-up check reject the scale faults I know of. Meters without phase voltage see no change.

Merge order

Base master. It does not depend on the fuse stack and merges cleanly into #1910 and #1914. Land it before #1914, which makes margin 0 the default. #1919 also touches fuse_heat.go and merges cleanly with this. #1789 rewrites phaseCurrentsA into SitePhaseCurrents; whichever lands later must carry the bound across.

🤖 Generated with Claude Code

Many P1 meters report phase current in whole amperes (DSMR 31.7.0), so a
phase can carry up to 1 A more than l*_a shows. At the fuse rating that
phase sat over In with no end. Take each phase's current as at least its
power over its measured voltage (|P| <= V*I), when the reading has both
and the phase powers add up to the total. The measured voltage keeps a
high grid from reading as extra current; a voltage outside 0.8-1.2 x
nominal bounds nothing.

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

chatgpt-codex-connector Bot commented Oct 28, 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-28T07:27:55.903415Z 4cefb73 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.

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