Skip to content

iPhone: the plan reaches the car the moment the box replans - #73

Merged
frahlg merged 3 commits into
mainfrom
claude/ios-plan-push
Oct 15, 2026
Merged

frahlg merged 3 commits into
mainfrom
claude/ios-plan-push

Conversation

@frahlg

@frahlg frahlg commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Problem

Fredrik plugged his car in at home. The box's page showed the new charging plan; the app did not. The app read /api/loadpoints (which carries the car's windows) only when the car started or stopped drawing power, and the box never sent its plan when it replanned. FTW Cloud's copy moved the cable but not the plan.

What this changes

Pair: srcfl/ftw#1643 replans the moment a cable goes in and pushes the session plan to streaming phones when a replan starts (pending) and when it lands, with chargers on it: per charger (loadpoint id) an optional carId, the plug state, pending/outdated, gridDeferred, goalRev, the windows ahead, the total and the shortfall. Keyed by charger, because in the car contract (srcfl/ftw#1647, srcfl/ftw-webapp#92) cars means real cars.

  • Plan.chargers and PlanCharger read it (nil from a box that sends none). PlanPushTests.thePlanWithChargersDecodesFromTheBoxsBytes decodes the bytes Core pins in TestThePlanWithChargersVector.
  • LoadpointsModel.takePlan() takes each charger's share the moment a plan arrives, with no read: the windows, cable, pending/outdated, gridDeferred and shortfall. While a new plan is made the windows stay those the box acts on, so Now's strip does not blink. Now's strip, the plan view, the car view and PlanShare (the widget record, which the watch gets with it) all draw from these, and PlanShare.check() takes the chargers before it writes.
  • A cable that moved (on the push or in FTW Cloud's copy), a charger the phone has not read, or a goal revision that moved (a goal set on the box's page or another phone) reads /api/loadpoints once.
  • A read that was on the wire while a newer push came keeps the push's cable and plan.
  • On a box that pushes, the shortfall comes with the plan, so the car view no longer reads /api/mpc/plan for it.

Fallback for a box without the push

A box whose plan comes without chargers is an older box (known once Now's plan.get answers). For it, the chargers' LiveAsk key also carries the quarter while a car is plugged in, and a 15 s step while it says plan_pending and for a minute after a cable moved: such a box notices a cable within 10 s, or 40 s after a replan of its own, and only then says pending. Until a plan has come the app does not guess, so a box that pushes never pays for the fallback.

The demo box pushes like the new Core (pushesPlan, on): pending when a replan starts after a cable, a goal, a level or solar-only, and the plan when it is made. A hold replans nothing, as in Core. answerDelayMs holds an answer back to test a stale read.

Measured

Laptop, loopback, a real ftwcloudd built from srcfl/ftw-cloud#15, the relay unreachable. The box ran in process with srcfl/ftw#1643's planner (Go DP), loadpoint manager on the event bus, plug-in replan, plan feed, app uplink, cloud link and tunnel, and served GET /api/loadpoints; Core's production reactive check stayed on (10 s, 30 s gap). The app was FTWKit's AppModel, paired through FTW Cloud with Now's asks on. "On screen" means LoadpointsModel has the car plugged in with its windows and PlanShare.entry has the charge. The harness is not in either repo.

median range
With the push, 8 plug-ins: plug-in → cable on the phone 3 ms 1–16 ms
plug-in → plan on screen 23 ms 20–39 ms
Old box with FTW Cloud's cable news, 6 plug-ins: plug-in → cable 173 ms 42–999 ms
plug-in → plan on screen 7.0 s 1.0–18.3 s

With the push: one read per plug-in, after the plan was on screen. The Go DP solved in a millisecond; on a Pi with Energyplan the plan lands when the solve ends, and the cable and "pending" still land at once. Old box: 1–3 reads per plug-in. Its worst case is the box's reactive check after a recent replan (up to 40 s) plus one 15 s ask, about 55 s; without FTW Cloud's copy, the next quarter or the car starting to draw. In the harness FTW Cloud's cable news was put in with no delay and seen at the app's next second.

Today, without these PRs, the plan reaches the phone only when the car starts drawing (23:00 in Fredrik's case) or a view opens after a minute.

Tests

PlanPushTests (FTWKit): the vector; a pushed plan moves the car's windows, PlanShare's charges and the plan with no read, keeps the windows while pending, and nothing reads the car again for a minute; a plug-in shows the cable and pending at once and reads once; a goal changed elsewhere is read once; a stale read keeps the cable a newer push gave; an older box is read once when FTW Cloud says the cable went in; an older box making its plan is asked every 15 s and stops when the plan is there. Whole FTWKit: 437 + 239 tests pass.

Files

Kept out of U3's and U4's files and step 2's carrier files. Touches SiteModel.swift by one word: cloudCars is internal(set) so a test can put FTW Cloud's cars in. Loadpoint.Window gets a public init for the demo. CLAUDE.md's read rule says what the push does.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 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-15T17:56:10.505002Z cac9fa1 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: cac9fa1295

ℹ️ 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 nextWindows != windows { windows = nextWindows }
if nextShortfall != shortfallWh { shortfallWh = nextShortfall }
shortfallReadAtMs = site.scheduler.nowMs
if let stamp, lps != stamp.value { self.stamp = Stamped(lps, asOfMs: stamp.asOfMs, origin: stamp.origin) }

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 pushed cable state against delayed cloud updates

When a plan push updates a car after the last charger read, this retains the old stamp timestamp. A delayed FTW Cloud car envelope whose source time falls between that read and the push therefore passes takeCloud's freshness check and can overwrite the newer pluggedIn value from the plan. The UI then shows the old cable state until the follow-up /api/loadpoints request succeeds—and indefinitely if that request fails—so the pushed plan needs separate freshness tracking that prevents older cloud car data from superseding it.

Useful? React with 👍 / 👎.

@frahlg
frahlg force-pushed the claude/ios-plan-push branch from cac9fa1 to fec57a7 Compare October 15, 2026 17:58
frahlg and others added 3 commits October 15, 2026 22:52
The app read the car's plan from /api/loadpoints only when the car
started or stopped drawing, so a car plugged in for a night charge kept
the old plan on the phone while the box's page showed the new one.

srcfl/ftw#1643 pushes the session plan when the box replans, with each
car's windows, cable, pending/outdated and shortfall. The app takes them
at once: Now's strip, the plan view, the car view and PlanShare (the
widgets and the watch) move with the push, and a cable that moved is
read once for the rest of the car. A box whose plan comes without cars
is read each quarter while a car is plugged in, and every 15 s while it
says the plan is being made or for 30 s after FTW Cloud says a cable
moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de it

srcfl/ftw#1643 now sends chargers/PlanCharger keyed by the loadpoint id,
with an optional carId (the session's car, empty until Core keeps cars),
since in the car contract (srcfl/ftw#1647) cars means real cars. The
vector Core pins carries carId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able

Review of app#73 (S1, N13) and the paired Core changes in srcfl/ftw#1643:

- Each charger on the plan now carries gridDeferred and goalRev. The car
  view takes gridDeferred from the push; a goal revision that moves (a
  goal changed on the box's page or another phone) reads the car once.
- While a new plan is made the push says pending and keeps the windows
  the box goes on acting on, so Now's strip does not blink.
- A read that was on the wire while a newer push came keeps the push's
  cable as well as its plan.
- An older box is asked every 15 s for a minute after a cable moved: it
  notices a cable within 10 s, or 40 s after its own replan.
- The demo box says pending when its replan starts, as Core now does,
  and can hold an answer back (answerDelayMs) to test a stale read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the claude/ios-plan-push branch from fec57a7 to 24af4aa Compare October 15, 2026 20:52
@frahlg
frahlg merged commit 899c5aa into main Oct 15, 2026
1 check 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