Repository navigation
iPhone: a Grid today widget, with today's energy over the grid, its cost and the day as a chart - #30
Conversation
Today's net energy over the grid with its direction, what was bought and sold, today's cost from the box, and the day as a chart on a fixed 00-24 axis. Small, medium and the lock screen's rectangle. - The totals and the cost are the box's own: GET /api/savings/daily?days=1, actual_cost_ore / 100. The app computes no cost. - The chart is the grid_w history the grid's node view reads, a quarter hour at a time. A hole stays a break. - GridTodayShare reads once a quarter hour while the app is on screen and writes one App Group record, as the price and plan widgets do. - After local midnight the widget says today's figures have not come yet. - The demo serves Core's savings fields from its own buckets. -FTWDemoWidgets YES lets a Debug demo leave its records for the widgets; -FTWDemoSunny YES gives a day that sells more than it buys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long cost cut the time short ("kl 18..."). The time says how old the
figures are, so it is never truncated.
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: 73cbe78227
ℹ️ 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".
| exportWh: totals.exportWh, | ||
| costMinor: totals.costMinor, | ||
| pricedCoverage: totals.pricedCoverage, | ||
| currency: prices.currency, |
There was a problem hiding this comment.
Wait for a known currency before caching the cost
When a non-SEK home opens, this load runs concurrently with PriceModel.load(), while PriceModel.currency falls back to "SEK" until that separate request completes (NowModels.swift). If the savings/history requests finish first—or the price request temporarily fails—the widget persists a correctly valued cost with the wrong kr unit, and it is not corrected until a later quarter-hour refresh. Use currency supplied by the savings response or defer/rewrite the record once the price model has an authoritative currency.
Useful? React with 👍 / 👎.
|
|
||
| func load() async throws { | ||
| let readAt = site.nowMs | ||
| let wire = try await site.callBox(.get, "/api/savings/daily", query: ["days": "1"]) |
There was a problem hiding this comment.
Prevent older grid loads from overwriting newer ones
If a request remains in flight across a quarter boundary—especially midnight—LiveAsk can start the newly wanted request without cancelling the previous one, but load() has no generation check. The older request can therefore finish last and overwrite the newer record; at midnight that leaves yesterday's entry as the latest cache and the widget grey until the next quarter-hour. Discard results whose request generation is no longer current before calling shareGridToday.
Useful? React with 👍 / 👎.
A user asked Fredrik for a widget like HomeWizard's "Strömtotal": today's energy from the grid, today's cost and a day chart. This PR takes that pattern into FTW's own look and words. It is called "Grid today" in English and "Nätet i dag" in Swedish.
What the widget shows
actual_cost_ore / 100from the box: import cost less export revenue, priced per quarter hour. A day that earned money shows a minus sign in the export colour. Below 0.95 of the day priced, the cost carries "delvis" / "partial". A day the box had no prices for says "inga priser" / "no prices" instead of a zero..monotonecurve with a soft fill below zero. The rest of the day stays empty, and a hole in the data stays a break. The y-axis shows 0 and at most one round value each way, and drops a label when its side is too thin to hold it.bolt.fill, the grid's own symbol on Now (NodeLook), so one thing keeps one look. It is in the iOS 26.5 runtime'sname_availability.plist(since 2019), and a test checks it and both arrows withNSImage(systemSymbolName:).systemMedium(the main one),systemSmall(the figures and a small chart) andaccessoryRectangularfor the lock screen (net, cost, both sides and the time). VoiceOver reads one sentence: "17,8 kWh netto från nätet i dag. in 21,8 · ut 4,0 kWh. kostnad 5,08 kr. kl 18:11".Colour: the requester asked for purple bars, but purple (
Theme.mobility) means the car in FTW, so the bars useimportingand the curveexporting, both set in one place (FTWWidgets/GridToday/GridColors.swift), and a test holds them toftw.importingandftw.exportingin the asset catalog.The widget follows the system like FTW's other widgets, with dark as the reference. On tinted and clear home screens it draws in one colour: the bars solid, the curve and its fill fainter, and zero tells them apart (
widgetRenderingMode,widgetAccentable).Where the numbers come from
Only the box's numbers. The app computes no cost.
GET /api/savings/daily?days=1:day,import_wh,export_wh,actual_cost_ore,priced_coverage_pct. The app may already read it (SimulatedBox's route table).grid_whistory the grid's node view reads (HistoryModel, five-minute tiles), folded into quarter hours. A quarter is the mean over the part the box has a record for; a quarter with no record has no value.session.box?.tz), and the chart's day is the day the box named in its answer.GridTodayShare(FTWKit) reads once a quarter hour while the app is on screen, and again when the household's day turns. Hidden, it asks nothing. Each read writes oneGridTodayEntryrecord (grid-today-<site>) throughSiteModel.write, as the price and plan records go, and the app wakes the widgets. The record carriescarrier,srcState(the grid meter's, at the read) and the time of the read, and reads back ascache.The demo
savings/dailywith Core's fields. Today comes from the same five-minute buckets its history serves, priced per quarter hour (import at the price, export at the spot share), so the totals and the chart add up. A test holds them within 3 %.-FTWDemoWidgets YES(Debug only) lets the demo leave its records for the widgets under the home "demo". Without it the demo still writes nothing; a test checks that too. This needed one flag onSiteModel.init(demoShares).-FTWDemoSunny YESgives a clear summer's day, which sells more than it buys.-FTWDemoPartial YESleaves a fifth of the day unpriced.Screenshots
From the demo on an iPhone 17 Pro simulator, iOS 26.5, in Swedish. Each picture holds the medium widget over the small one. "Bought" is an October evening; "Sold" is the same evening with
-FTWDemoSunny YES.The lock screen always draws widgets in one colour, so it has no light, dark or tinted version of its own. The "delvis" mark, dark:
How they were taken: the widgets went onto the home screen through
IconState.plistbefore boot, and the app ran once with-FTWDemo YES -FTWDemoWidgets YES(and-FTWDemoSunny YES) before each picture.FTWUITests/GridWidgetTourpicks the icon style (Förval, Mörkt, Klart, Tonat), places the widget on the lock screen and takes the pictures. It is skipped unless asked for (TEST_RUNNER_FTW_GRID=…) and drives SpringBoard with Swedish labels. I also checked the largest text size, where the small widget's time moved to its own line so a long cost can no longer cut it short, and the grey new-day state, by dating the record a day back.Tests
swift testinappleApp/FTWKit: 379 tests in 54 suites, green (105 in FTWSharedTests, 274 in FTWKitTests), 23 of them new:GridGlanceTests(16): decimals in sv and en, the arrow and a figure with no sign, the negative cost and a minus that rounds to nothing, the partial and no-prices marks, the day boundary (yesterday after midnight, a 25-hour day's axis), bars and the curve with a hole, two runs sharing a zero, the quiet y-axis, quarters from history steps, the snapshot round trip, every word in Swedish, and the SF Symbols.GridTodayShareTests(7): the demo fills every part of the widget and its totals match its chart, a sunny day sells more than it buys, a partly priced day, the demo alone writes nothing, one read per quarter hour and none while hidden, the box's row as it gave it, and the colours against the asset catalog.The app and the widget extension build for the iOS Simulator (
xcodebuild build -project FTW.xcodeproj -scheme FTW -destination 'generic/platform=iOS Simulator'). Now andNowFitsTestsare unchanged.Coordination
FTWWidgets/GridToday/. The bundle inPriceWidget.swiftgains one line,GridTodayWidget(), afterChargeNowLiveActivity(). iPhone plan dial: today and tomorrow round the sun's path, in the plan view and as a widget #27 (the plan dial) addsPlanDialWidget()afterPlanWidget(); keep both lines if they meet.origin/mainat 5fc8ea0 (chore(ios): version 0.3.1 #29). The pictures come from 420eeb0, one merge before; that merge touched no widget or demo code, and the merged build writes and draws the same record.Not in this PR
FTWApp.swiftand a later change.savings/dailywith no days. The app then writes nothing, and the widget asks the reader to open FTW.🤖 Generated with Claude Code