Repository navigation
feat(appproto): stream the grid's phase currents and the main fuse - #1629
Conversation
Lane 0 and FTW Cloud's live body gain extra fields 11 to 14 from the registry: the site meter's current on L1, L2 and L3 in tenths of an ampere, signed by the site convention, and the main fuse per phase. A meter that gives only the size of each current gets its sign from the power on the same phase, which the driver already signed. Only the site's phases go, and only while the meter is online. A field that stops coming makes the box resend the snapshot once, because a delta cannot carry an absence. The generator learns extra_fields and refuses an id or name that means two things. 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: aaf88b3201
ℹ️ 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 w := watts[i]; w != nil && finite(*w) && *w != 0 { | ||
| a = math.Copysign(math.Abs(a), *w) | ||
| } |
There was a problem hiding this comment.
Normalize phase-current signs in the driver
When a driver already supplies signed l*_a and its nonzero l*_w has the opposite sign, this discards the driver's current sign and reports the opposite direction to both the app and FTW Cloud. Preserve the normalized driver value here and fix magnitude-only vendors at their driver boundary instead; Core-side Copysign violates the project's explicit sign-conversion invariant.
AGENTS.md reference: AGENTS.md:L63-L64
Useful? React with 👍 / 👎.
WP16 of the iPhone polish plan, step 2 of 3. On 14 October 2026 Fredrik asked to see the grid's phases, L1, L2 and L3, live against his main fuse. The box reads them every second since Sungrow 1.6.0 (#1619), but lane 0 and FTW Cloud's live body carried no phase. This sends them.
What changes
contract/registry.yamlgainsextra_fieldsafter the frozen set:grid_l1_da,grid_l2_da,grid_l3_da(11–13) andgrid_fuse_da(14), all in tenths of an ampere. Copied byte for byte from feat(contract): the grid's phase currents and the main fuse as extra fields ftw-webapp#86, the source copy.gencontractreads the block into the sameFidtype, addsFieldNamesandFieldUnitsfor every id, keepsFrozenFieldNamesfor the frozen set alone, and refuses an id or a name that sits in both blocks, or an extra id among the frozen ones.SnapshotgainsGridPhaseAandFuseA.fieldValuesturns them into fields 11–14, so lane 0 and FTW Cloud's live body carry the same numbers. The dictionary points each phase at the site meter, so a phase is exactly as fresh as the meter. The fuse is config and points at no source. FTW Cloud'sfieldSourcesnames the meter for 11–13.fuse.phases, in L1 to L3 order, as the fuse guard reads them), each only while the meter reports it, and none while the meter is offline. The fuse goes only with at least one phase: a scale with nothing on it says nothing.gridPhaseAmpscopies its sign onto the current's size. Otherwise the driver's own sign stands. This converts no vendor sign: it ismath.Copysign(|a|, w), so a current the driver already signed keeps its sign and nothing can flip twice. A meter that gives neither sends positive values both ways; the registry says a positive current alone does not prove import.Ticknow resends the snapshot once when a field it sent is missing, the same way a newcontrolRevdoes. Both clients replace their whole field set onsnap. This also covers a charger leaving field 10.fitDeltaalready drops the highest id first, so 11–14 go before the core view; a test proves the order.Hardware evidence
The home box (v0.142.1-beta.1, Sungrow SH10RT as site meter, 20 A, three phases), read-only over ZeroTier, 14 October 2026 18:27:56–18:29:10. I have not run this branch on the box: that needs Fredrik's yes. Instead, 75 readings of
/api/status(phase_amps,phase_powers,fuse), one a second, were fed through this branch's own code: a realappSiteover a realtelemetry.Store, a realappproto.Handlerwithhelloandsub,Tickonce per reading, and every frame it sent decoded back withappuplink.Codec(). The phone's view is the snapshot plus each delta.Every size is within half a tenth of the box's own, every sign matches the box's phase power, and all 75 lane 0 frames are 512 bytes. The battery was giving 3.4 kW through a three-phase inverter while the house drew on L2, so L1 and L3 sold while L2 bought: one phase near 7.7 A while the grid read near zero. The replay harness was a throwaway test file and is not in this branch.
FTW Cloud
An owner who shares live readings with FTW Cloud (
ftw.live.read) now also shares the three phase currents and the fuse, at the same rate as the grid power.docs/protocol-cloud.mdand thecloudwire.Livecomment say so. FTW Cloud forwards the field map without reading ids, so the service needs no change.Contract-pair: srcfl/ftw-webapp@feat/grid-phases
Paired pull requests. All three carry the same registry, byte for byte.
Merge this one and srcfl/ftw-webapp#86 together. The changeset is a patch: nothing changes for a user until the app draws the fields.
Not in this pull request
sungrow.luasetsl1_ato 0 when register 5743 does not answer (optional_readgives nil), so a missed read looks like an idle phase rather than a missing one. Core's per-phase fuse guard reads the same 0. Leaving the keys out on a failed read belongs in srcfl/device-drivers.status.phasesexists in the registry, and no box sends it. The app gates on the fields themselves, so this leaves it alone.Tests
🤖 Generated with Claude Code