Repository navigation
fix(loadpoint): keep the EV ready time on the household clock - #1582
Conversation
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>
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: 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".
| func zone(loc *time.Location) *time.Location { | ||
| if loc == nil { | ||
| return time.Local | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
| 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() |
There was a problem hiding this comment.
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 👍 / 👎.
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>
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_utcstill goes both ways:localHHMMToUtcMinsinweb/app.js,localInputToUtcMinutesin the webapp, the Swift draft), so 07:00 in the browser stays 07:00 in Core.The on-box UI, ftw-webapp and the iOS draft (srcfl/ftw-app#5) need no change, so there is no paired PR.
registry.yamldoes 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.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
Zone
The household zone is the box's zone (
time.Local, from the host's/etc/localtimeorTZ), as for the weekday mask, daily savings and plan days. A box running in UTC (for example an old Docker install withoutTZ) 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
go/internal/loadpoint/schedule_dst_test.go, all inEurope/Stockholm:time_of_day_min_utc360 in winter and 300 in summer.time_of_day_min_utc.time.Localto 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. Theloadpoint,api,state,mpc,unitsandcmd/ftwsuites pass withTZset to UTC, Europe/Stockholm and Australia/Sydney (in DST now, the opposite season to May).go test -race ./internal/loadpointpasses.TZ=Europe/Stockholm make verifypasses.TZ=Europe/Stockholm, agarageloadpoint and an old row{"soc":0.8,"time_of_day_min_utc":300,"recurring":true,"days":31}in state.db:"time_of_day_min":420;GET /api/loadpointskept the same schedule fields as before (time_of_day_min_utc: 300, no new field).time_of_day_min: 390, answeredtime_of_day_min_utc: 270and set the next deadline to 04:30 UTC (06:30 CEST).For the owner to decide (not in this PR)
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.localInputToUtcMinutesinsrc/lib/format/ev.tssays a DST change moves the deadline. After this PR that is no longer true; a one-line fix in ftw-webapp.go/cmd/ftw/app_link.gosendstime.Now().Location().String()as the box zone. WithoutTZset, Go names that zoneLocal, not an IANA name, so the app gets"Local".forecastTimezone()already resolves the real name.🤖 Generated with Claude Code