Skip to content

feat(ios): the energy tax refund after battery storage, for Skatteverket - #145

Merged
frahlg merged 3 commits into
mainfrom
claude/ios-battery-tax-refund-bffb30
Nov 2, 2026
Merged

frahlg merged 3 commits into
mainfrom
claude/ios-battery-tax-refund-bffb30

Conversation

@frahlg

@frahlg frahlg commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

A Swedish household gets back the energy tax on grid electricity that a battery stored and later sent back to the grid (11 kap. 13 § LSE). Core works out the figure for one calendar year in srcfl/ftw#1950, with an account per battery from srcfl/ftw#1953. The two ship in the same Core beta. Fredrik asked on 2 Nov 2026 to carry it into the iOS app.

What changes

  • History gets one row under the day: Energy tax refund / Energiskatt tillbaka. It shows only for a home whose price area is SE1 to SE4 and that has a battery. The row asks the box nothing; the screen behind it asks.
  • The screen shows one calendar year, last year first:
    • the figure in whole kWh, rounded down, as the box's own page writes it;
    • complete hours of all hours, and "The year is still running" for this year;
    • the battery's size and the least round-trip efficiency the box counted with; with several batteries, a line for each with what came from it, its size and its efficiency, as the box's own History card writes it;
    • each month with its figure and complete hours;
    • the hourly basis as ftw-battery-return-<year>.csv, shared through the system share sheet;
    • that FTW counts low where readings are unclear, that the figure assumes FTW reads every inverter, battery and charger, that an owner can apply for three years after the year ends and the box keeps the hourly readings that long (feat(state): the energy ledger keeps hourly totals for four years ftw#1951), and a link to Skatteverket's rules.
  • A year menu offers this year and the three before it.
  • When the box sets problem, the screen shows its reason and no figure, and no months: battery_size names the batteries without a size, battery_unread names the drivers in unread, and any reason a later box adds gets a plain sentence.

How it reads the box

Core needs no change. Both routes are Read in api.routes on srcfl/ftw#1950 and #1953, and the passthrough carries text/csv (carriedMedia takes text/*). So the app calls GET /api/energy/battery-return?year= and .csv?year= through api.req. No registry name changes. A box without #1950 answers E_UNKNOWN_OP, and the screen says the box may run older software.

  • BatteryReturn (FTWKit/Format) decodes Core's JSON (the batteries list and unread of #1953) and holds the rules: Swedish price area, Swedish calendar years, whole kWh.
  • BatteryReturnGlance builds what the screen says, once per answer, year or minute.
  • BatteryReturnModel asks through LiveAsk, named by year and hour: once while the screen is open, again for another year or a new hour, never on a timer. It keeps each year's last answer in the sealed home file, so the screen opens on it, dated (SAVED ON THIS PHONE · AS OF …). The file is at version 2, so an answer kept in #1950's first shape is dropped.
  • The CSV is read from the box when the share sheet asks for the file, not when the screen opens. SiteModel.readBox returns the raw body.
  • Words in English and Swedish in BatteryReturnWords; a test fails on any sentence without Swedish.
  • The demo box answers both routes in #1953's shape, with the CSV one row per battery and hour. Two years back it read two batteries; its oldest year has a size for a battery it read nothing from, so the demo shows a reason too.
  • -FTWTab refund opens the screen in debug builds, and -FTWRefundYear 2024 picks the year; scripts/demo-screenshots.sh shoots both cases.

Files that open PRs touch: only HistoryTab.swift overlaps (#107). This PR adds three lines to the tab's body and a new view at the end of the file.

Evidence (this Mac)

  • swift test in appleApp/FTWKit: 659 Swift Testing tests and 348 XCTest tests green, 16 of them new (BatteryReturnTests, BatteryReturnModelTests).
  • xcodebuild for the iOS Simulator: builds with no warnings in the new code.
  • Driven in the simulator (iPhone 17 Pro, iOS 26.5, demo): the row opens the screen, the year menu switches years, and Save to Files from the share sheet wrote ftw-battery-return-2025.csv with a header and 8,760 hours. After #1953: 2024 shows two batteries and 2023 the battery_unread reason, in English and Swedish and at the largest text size. A test reads the 2024 CSV: 2 × 8,784 rows, one per battery and hour.
  • Launch is untouched, so no LaunchClock numbers.
History, dark 2025, dark 2025, light
Months and notes Year menu Share sheet
2026 so far Swedish, dark Swedish, light Swedish, notes

One account per battery (srcfl/ftw#1953):

2024: two batteries 2023: a battery not read Swedish, two Swedish, not read Largest text

Largest text size:

Top, dark Top, light Months, dark History row, light

For review

  • The battery check is mine, beyond the brief: a Swedish home with no battery has nothing to claim, so it sees no row.
  • The Swedish title is Energiskatt tillbaka; the longer Återbetalning av energiskatt did not fit the bar.
  • Real boxes need feat(history): the figure for Skatteverket's energy tax refund after battery storage ftw#1950 and #1953 merged and released first. A box with #1950 alone would show no battery line, since its top-level efficiency and capacity_wh are no longer read; no released box answers that way. The note that the box keeps the hourly readings for the three years an owner can apply is true once feat(state): the energy ledger keeps hourly totals for four years ftw#1951 is in too; it matches the box's own History note there.
  • #1950's later fix (b0770337, a device whose identity changed counts as one meter) keeps the JSON shape, so the app needs nothing for it.
  • The test of a second launch waits for the home file to land: the file is sealed off the main actor, and under load the second launch could read before it. SavingsModelTests.itOpensOnTheLastAnswer has the same gap.
  • Do not merge before Fredrik says so.

🤖 Generated with Claude Code

A Swedish household gets back the energy tax on grid electricity a
battery stored and later sent back to the grid. Core works out the
figure for one calendar year (srcfl/ftw#1950). History now has a row
for it under the day, for a home in SE1 to SE4 with a battery. The
screen shows the year's figure in whole kWh rounded down, the hours
behind it, the battery it was counted with and each month, and shares
the hourly basis as CSV. A problem from the box shows its reason and no
figure. Both routes are Read in Core, so the session's API passthrough
carries them as they are.

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

chatgpt-codex-connector Bot commented Nov 2, 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-11-02T09:51:57.663084Z 8e20428 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.

frahlg and others added 2 commits November 2, 2026 11:18
srcfl/ftw#1951 keeps the ledger's hourly totals for four years, so the
refund's note now says the box keeps them for the three years an owner
can apply, as the box's own History does. The test of a second launch
waits for the home file to land before it reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core now counts each battery on its own (srcfl/ftw#1953), and #1950 and
#1953 ship in the same beta. The answer lists the batteries with their
size and least efficiency, names the drivers it read nothing from in
`unread`, and drops the top-level `efficiency`, `capacity_wh` and the
`batteries` count with its problem.

- Decode the list and `unread`. One battery keeps its line; with several,
  each has a line with what came from it, its size and the least it gives
  back, as the box's own History card writes it. The figure stays the
  box's total.
- `battery_size` names the batteries without a size; `battery_unread`
  names the drivers in `unread`, in English and Swedish. Any other reason
  keeps the plain text.
- The kept answers move to version 2, so one in the old shape is dropped.
- The demo box answers in the new shape and writes the CSV one row per
  battery and hour. Two years back it read two batteries; the oldest year
  has a size for a battery it read nothing from.
- Debug builds take `-FTWRefundYear`, and the screenshot script shoots
  both cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 3591e80 into main Nov 2, 2026
1 check passed
@frahlg frahlg mentioned this pull request Nov 2, 2026
frahlg added a commit that referenced this pull request Nov 2, 2026
Release notes in English and Swedish for #145 (the energy tax refund
after battery storage) and #147 (Hannes's three notes on the plan's
clock and the level). The two 0.5.x paragraphs go, so both files stay
under App Store Connect's 4000 characters.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Nov 3, 2026
Fredrik, 3 Nov 2026: the refund for battery storage does not belong in
the iPhone app. This reverts 3591e80 (#145). Core keeps
/api/energy/battery-return and its History tile on the box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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