Skip to content

fix(loadpoint): keep the EV ready time on the household clock - #1582

Merged
frahlg merged 1 commit into
masterfrom
fix/ev-departure-local-time
Oct 12, 2026
Merged

frahlg merged 1 commit into
masterfrom
fix/ev-departure-local-time

Conversation

@frahlg

@frahlg frahlg commented Oct 12, 2026

Copy link
Copy Markdown
Member

Problem

Core stored the EV ready time as a UTC minute of day (time_of_day_min_utc). A goal set to 07:00 in summer is 05:00 UTC. When Sweden leaves summer time on 2026-10-25, 05:00 UTC is 06:00, so the car is ready an hour early and charges in dearer hours. On 2027-03-28 it turns the other way and the car is an hour late. The time stayed wrong until the owner saved again.

With a weekday mask the error grows. A Monday-only 00:30 goal is Sunday 22:30 UTC in summer. After 2026-10-25 that is Sunday 23:30, so the mask skipped it and the car was due on Monday at 23:30, almost a day late.

Change

Core keeps the ready time on the household clock: minutes after midnight in the box's zone, the same zone the weekday mask already uses. state.db stores it as time_of_day_min.

The API does not change. time_of_day_min_utc still goes both ways:

  • A client's UTC minute is read on today's date. That is how every client makes it today (localHHMMToUtcMins in web/app.js, localInputToUtcMinutes in the webapp, the Swift draft), so 07:00 in the browser stays 07:00 in Core.
  • Core answers with the UTC minute of the ready time on today's date. Clients turn it back with today's offset and show 07:00 in summer and winter.

The on-box UI, ftw-webapp and the iOS draft (srcfl/ftw-app#5) need no change, so there is no paired PR. registry.yaml does not hold this name.

Saved goals

A row saved before this change holds only a UTC minute, which does not say what offset it was saved with. Core reads it on the boot date, as the time the owner sees that day, and saves it back once with time_of_day_min. From then on DST changes do not move it.

  • Updated before 2026-10-25: a goal saved in summer keeps the time the owner set (05:00 UTC → 07:00).
  • Updated after 2026-10-25: the goal keeps the time the owner has seen since the change (05:00 UTC → 06:00). Core cannot tell that the owner meant 07:00; the owner saves 07:00 once and it holds.

So this should reach beta boxes before 2026-10-25 if possible.

Stored rows keep time_of_day_min_utc, so a rollback to an older Core still finds a ready time. If that older Core saves again, the next update reads the row again.

Edge rules

  • A time the clock skips (02:30 on 2027-03-28) is due when the clock jumps, 03:00.
  • A time the clock repeats (02:30 on 2026-10-25) is due the first time, and only once that day.
  • On 2026-10-25 a UTC minute from an old client between 22:00 and 22:59 matches both 00:xx and 23:xx local. Core takes 00:xx. This needs an old client saving a 23:xx goal on that one day.

Zone

The household zone is the box's zone (time.Local, from the host's /etc/localtime or TZ), as for the weekday mask, daily savings and plan days. A box running in UTC (for example an old Docker install without TZ) keeps today's behaviour: no DST, so the ready time still moves an hour for its owner. The fix there is to set the box's zone. Any EU zone works, because EU zones switch at the same moment.

Evidence

  • New tests in go/internal/loadpoint/schedule_dst_test.go, all in Europe/Stockholm:
    • 07:00 stays 07:00 across 2026-10-25 (05:00 → 06:00 UTC) and 2027-03-28 (06:00 → 05:00 UTC).
    • A Monday-only 00:30 goal stays Monday 00:30 across both changes.
    • Skipped and repeated 02:30.
    • What clients read: 07:00 is time_of_day_min_utc 360 in winter and 300 in summer.
    • An old row read before and after 2026-10-25, saved back, then read again in the next season without a further save.
    • The stored form keeps the clock, the API form does not, and a rollback reads time_of_day_min_utc.
  • The two behaviour tests fail on current master: 05:00 instead of 06:00 UTC on 2026-10-25, and Monday 22:30 UTC (23:30 local) instead of Sunday 23:30 UTC (Monday 00:30 local).
  • Loadpoint tests now pin time.Local to UTC unless a test sets a zone. A client's UTC minute is read on its save date, so the machine's zone and season would otherwise move fixed-date deadlines. The loadpoint, api, state, mpc, units and cmd/ftw suites pass with TZ set to UTC, Europe/Stockholm and Australia/Sydney (in DST now, the opposite season to May). go test -race ./internal/loadpoint passes.
  • TZ=Europe/Stockholm make verify passes.
  • On-box UI checked in a browser against a local Core built from this branch, with TZ=Europe/Stockholm, a garage loadpoint and an old row {"soc":0.8,"time_of_day_min_utc":300,"recurring":true,"days":31} in state.db:
    • At boot Core rewrote the row with "time_of_day_min":420; GET /api/loadpoints kept the same schedule fields as before (time_of_day_min_utc: 300, no new field).
    • The EV dialog showed "80 % by 07:00 · repeats" and the editor showed 07:00, Mon–Fri.
    • Setting 06:30 in the editor showed "Schedule saved." and "80 % by 06:30 · repeats"; Core stored time_of_day_min: 390, answered time_of_day_min_utc: 270 and set the next deadline to 04:30 UTC (06:30 CEST).
    • The browser cannot move the date past 2026-10-25, so the change itself is covered by the tests above, not the browser check.

For the owner to decide (not in this PR)

  1. Send the household clock on the wire? The API could add time_of_day_min (and accept it). Clients could then show the box's time without converting through the phone's zone, which matters for a phone abroad and for the iOS app. That changes the wire format, so I have not done it.
  2. The webapp comment on localInputToUtcMinutes in src/lib/format/ev.ts says a DST change moves the deadline. After this PR that is no longer true; a one-line fix in ftw-webapp.
  3. go/cmd/ftw/app_link.go sends time.Now().Location().String() as the box zone. Without TZ set, Go names that zone Local, not an IANA name, so the app gets "Local". forecastTimezone() already resolves the real name.

🤖 Generated with Claude Code

Core stored the ready time as a UTC minute of day, so 07:00 became
06:00 when summer time ended and 08:00 when it began, until the owner
saved again. Sweden leaves summer time on 2026-10-25.

Core now keeps the time on the household clock (the box's zone, the
same zone the weekday mask uses) and stores it in state.db as
time_of_day_min. The API keeps time_of_day_min_utc in both directions:
a client's UTC minute is read on today's date, and Core answers with
today's UTC minute, so existing clients need no change.

A row saved before this change holds only a UTC minute. It is read on
the boot date, the time the owner sees then, and saved back once.
Stored rows keep time_of_day_min_utc so a rollback still works.

A time a DST change skips resolves to the end of the skipped hour; a
repeated time resolves to its first occurrence.

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

chatgpt-codex-connector Bot commented Oct 12, 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-12T14:06:05.085019Z 207299d 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: 207299d02c

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

Comment on lines +217 to +220
func zone(loc *time.Location) *time.Location {
if loc == nil {
return time.Local
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass the household zone to Docker installations

In the supported 0.x Docker path, deploy/docker/Dockerfile installs tzdata but deploy/docker/compose.yaml supplies neither TZ nor /etc/localtime, so Debian leaves time.Local as UTC; main also never calls SetLocation. Consequently, a Swedish browser sending 05:00 UTC for a 07:00 goal is stored as 05:00 on the supposed household clock, and after the DST change the browser shows 06:00—the original regression remains for every non-UTC Docker household. Resolve the household location in Core or configure it in the supported Compose deployment instead of relying on this fallback.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

Comment on lines +194 to +198
func utcMinuteToday(now time.Time, clock int, loc *time.Location) int {
loc = zone(loc)
y, mo, d := now.In(loc).Date()
u := wallClock(y, mo, d, clock, loc).UTC()
return u.Hour()*60 + u.Minute()

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 skipped-hour values across API round trips

On the spring-forward date, a persisted recurring 02:30 goal reaches this conversion through Manager.snapshot; wallClock normalizes it to 03:00, so utcMinuteToday returns the UTC minute that web/app.js:4645-4648 renders as 03:00. If the owner opens and saves the goal that day—even to change only its SoC—the client sends 03:00 back and clockFromUTC permanently replaces the durable 02:30 schedule with 03:00 for subsequent days. The wire representation needs to preserve the requested household-clock minute rather than the transition day's effective execution instant.

AGENTS.md reference: AGENTS.md:L28-L30

Useful? React with 👍 / 👎.

@frahlg
frahlg merged commit 851eaee into master Oct 12, 2026
18 checks passed
frahlg added a commit to srcfl/ftw-webapp that referenced this pull request Oct 12, 2026
Read time_of_day_min when the box sends it, so a phone in another time
zone shows and saves the time at home. Send it with the UTC minute, so
an older box still works. The sim keeps the household clock the way the
box does. The old note that a DST change moves the deadline was wrong
since srcfl/ftw#1582.

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