Skip to content

Stop drawing a stale meter as 0 W balanced - #980

Merged
frahlg merged 3 commits into
masterfrom
cursor/stale-meter-flow-honest-5640
Aug 26, 2026
Merged

Stop drawing a stale meter as 0 W balanced#980
frahlg merged 3 commits into
masterfrom
cursor/stale-meter-flow-honest-5640

Conversation

@frahlg

@frahlg frahlg commented Aug 26, 2026

Copy link
Copy Markdown
Member

Accepted text proposal

Fredrik's NOW screenshot: GRID and the house at 0 W "balanced", solar and battery gone, while the headline already said control is paused because a meter stopped reporting.

What changed

  • GET /api/status publishes grid_w and load_w as JSON null when the site meter is offline. A live 0 W reading is unchanged. Development setups with no configured meter still report 0.
  • Offline driver details still include last-known pv_w / bat_w so the UI can keep those nodes.
  • Flow mapping moved to web/components/energy-flow-readings.js:
    • null grid_w → GRID "no data", not "balanced"
    • null load_w → house "—"
    • if every driver of a role is offline, that role stays on the diagram as a placeholder
    • a spare offline inverter next to live solar still stays hidden
  • <ftw-energy-flow> treats an explicit load: null as unknown (does not keep the last 0 W).
  • Values tiles match: solar / grid / home / battery show "—" and "no data", not generating / 0% SoC.

Why

The meter, PV and battery are often the same hybrid inverter. Skipping it as offline left GRID + EV at 0 W and looked like the house never had solar. Publishing 0 W for a missing meter made the same lie in numbers.

Boundaries and safety

  • Planner output is still not sent to hardware. Dispatch already stops on a stale meter; this only stops pretending the site is idle.
  • Faulted-but-still-emitting meters keep their reading (existing behaviour).
  • The 1 Hz app stream still encodes grid/load as 0 when the meter is down; DispatchBlockedBy remains the honesty channel on that path. The screenshot was the /api/status path (daily kWh on GRID).
  • The phone app (srcfl/ftw-webapp) already maps null grid_w to GRID "no data", but still draws 0 W, coerces load_w to 0, and skips offline inverters. I could not push a companion PR (no write access to that repo). After this box change, apply the same mapper rules there: placeholder: true on no-data planets, keep offline-only PV/battery, load: null, and re-copy ftw-energy-flow.js from this branch so the hub draws "—".

Overlapping PRs

Verification

  • go test ./internal/api/ -run TestHandleStatus
  • go vet ./... and go build ./... in go/
  • node --test web/energy-flow-readings.test.mjs web/dashboard-simplification.test.mjs web/javascript-syntax.test.mjs
  • Browser: dashboard fed the screenshot payload (offline ferroamp + live idle charger). Flow shows house/GRID/SOLAR/BATTERY as "—" / "no data"; EV stays 0 W idle. Values tiles match after a hard refresh.

make verify was not run to completion here: optimizer venv needs python3-venv, which this environment does not have.

Checklist

  • The diff implements one accepted scope and does not add follow-on work.
  • I checked open pull requests that touch the same files.
  • Tests cover the changed behaviour and its failure path.
  • A human reviewed every changed web/UI view in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.
Open in Web Open in Cursor 

cursoragent and others added 3 commits August 26, 2026 12:32
A hybrid inverter that is also the site meter going offline left
GET /api/status publishing grid_w/load_w as 0. The dashboard and the
FTW app treated that as a live idle house, hid solar and battery, and
labelled GRID "balanced".

Publish JSON null when the meter is not reporting, map that to "no
data", and keep an offline-only PV/battery/EV role on the diagram as a
placeholder.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
The Flow view already drew placeholders. The Values cards still labelled
offline solar as generating and showed 0% SoC for a pack that is not
reporting.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg marked this pull request as ready for review August 26, 2026 12:47
@frahlg
frahlg merged commit b1384b3 into master Aug 26, 2026
16 checks passed

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c94a11c. Configure here.

Comment thread web/app.js
// Solar only flows one direction (production); the arrow would
// be redundant. Use the kWh unit instead so the line reads as
// a standalone total.
var pvDailyStr = fmtKwhShort(pvKwhTotal) + " kWh";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Live stats still show fake zeros

Medium Severity

The Values tiles now show / no data when configured solar or battery drivers are offline, but the live stats strip still feeds site-level pv_w, bat_w, and bat_soc (which become 0 once offline drivers are excluded). That strip is meant to mirror the tiles, so it keeps drawing the same idle/empty lie this PR removes elsewhere.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c94a11c. Configure here.

let selfPoweredPctToday = null;
if (loadKwhTotal > 0.001 && loadW !== null) {
selfPoweredPctToday = Math.max(0, Math.min(100, (1 - importKwh / loadKwhTotal) * 100));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Today % gated on live load

Medium Severity

selfPoweredPctToday is computed only from today’s import_wh / load_wh, but the new mapper also requires instantaneous load_w to be non-null. When the site meter goes offline, the hub drops % SELF-POWERED TODAY even though the daily energy totals are still valid.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c94a11c. Configure here.

@cursor cursor 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.

Not approved: Cursor Bugbot finished skipped and left 2 unresolved medium findings (live stats still showing fake zeros; today % gated on live load). Human review is needed; assigning reviewers for the web/status surfaces.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot requested review from Leitet and erikarenhill August 26, 2026 12:51

@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: c94a11c7c6

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

const gIdle = isIdle(gridW, thresholdW);
planets.push({
id: "grid", corner: "bottom-left", title: "GRID", role: "grid",
kw: Math.abs(gridW) / 1000, toHub: gridW >= 0,

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 Preserve the negative sign for grid export

When grid_w < 0, applying Math.abs makes the grid bubble display a positive power value even though it is labeled “exporting”; the previous mapper and the Values/chart views preserve the negative value. Keep the signed kW value and use toHub only for animation direction so an export is not presented as positive site power.

AGENTS.md reference: AGENTS.md:L32-L33

Useful? React with 👍 / 👎.

Comment on lines +214 to +215
if (loadKwhTotal > 0.001 && loadW !== null) {
selfPoweredPctToday = Math.max(0, Math.min(100, (1 - importKwh / loadKwhTotal) * 100));

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 Keep valid daily self-powered history during an outage

If the meter goes offline after accumulating valid readings, /api/status still returns energy.today, but the new loadW !== null condition discards the independently calculated historical percentage. Consequently the component hides “SELF-POWERED TODAY” for the entire outage even though import_wh and load_wh remain available; only the live “now” calculation should depend on the current meter.

Useful? React with 👍 / 👎.

Comment thread web/app.js
Comment on lines +709 to +710
if (flowEl && typeof window.ftwFlowReadingsFromStatus === "function") {
lastFlowReadings = window.ftwFlowReadingsFromStatus(data, {

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 Replay status after the mapper module becomes ready

On a cold load where the three /api/* reads finish before the large components/index.js module graph has evaluated, this condition drops the first successful status payload because the readiness callback is nested inside the false branch's body. The flow component then remains in its loading skeleton until the next two-second poll; cache the payload independently and derive/replay it when the module or custom element becomes ready.

Useful? React with 👍 / 👎.

HuggeK added a commit to HuggeK/ftw that referenced this pull request Aug 29, 2026
Master moved from srcfl#756 to srcfl#998 under the branch; the only real conflict
was firstboot.sh, where srcfl#980-era work made first boot try `docker
compose up -d` on locally present images before falling back to the
GHCR pull loop. Kept that up-first flow and placed the docker.list
restore (srcfl#770) before it, right after cd /opt/ftw.

Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
Claude-Session: https://claude.ai/code/session_012taMkhFbYYNyVQmCTU26sn
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.

2 participants