One schedule for every charger, and a clock on the server to keep it

The charger's own cloud schedule is one window inside one box: charge
between these hours, every day, and that is the whole vocabulary. A third
tab on Charging holds a list instead — each line an action, a time, the
days it repeats on and the chargers it acts on — and one list covers the
whole account rather than each charger hiding its own.

The clock is the server's. A schedule that only fires while a tab is open
is a reminder, so a ticker sweeps every enabled task and fires whichever
minute has come. It sends by handing a synthesised request to the same
control endpoint the page's buttons use, so a scheduled command goes
through the same cascade, ownership gate, rate limit and audit trail —
what the owner cannot press by hand, the scheduler cannot send for them.

A task names its chargers, or names none, which means all of them and
keeps meaning that for a charger imported next year. Times are stored as
a wall clock plus the zone they were written in, so 23:00 stays 23:00
wherever the server sits. One action per task: a charging window is the
two tasks that open and close it, which is how it is read back, edited
and switched off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-09-03 22:47:30 +02:00
1 parent 8e4c22cfcf
commit 5a4515978f
15 files changed
+1856 -7

No files matched your search

+42
View File
@@ -152,6 +152,48 @@ type HomeCharger struct {
Created string `json:"created,omitempty"`
}
// ChargingTask is one line of the home-charger scheduler: an action, a time of
// day, the days it repeats on, and the chargers it acts on. It belongs to the
// person, like the chargers themselves — one list covering every charger they
// own, rather than a separate schedule inside each one.
//
// The charger's own cloud schedule can only say "charge between these hours,
// every day, on this one box". This says "at 23:00 on weeknights, cap these two
// chargers at 10 A" — several tasks, each naming its own chargers.
type ChargingTask struct {
ID string `json:"id"`
Name string `json:"name"`
// The home-charger records this task acts on. Empty means every charger the
// owner has, including ones imported after the task was written — "all of
// them" is a standing wish, not the list that happened to exist that day.
Chargers []string `json:"chargers"`
// What to do: start, stop, limit (to Amps) or boost. One action per task; a
// charging window is the two tasks that open and close it, which is also how
// it is edited and how it is switched off.
Action string `json:"action"`
Amps float64 `json:"amps,omitempty"`
// When, as a 24-hour "HH:MM" read in Zone — the IANA zone the browser was in
// when the task was written. The server's own clock is not the one the user
// set the time by, and a laptop that travels must not move the schedule.
Time string `json:"time"`
Zone string `json:"zone,omitempty"`
// The weekdays it repeats on, 0=Sunday … 6=Saturday. Empty means every day.
Days []int `json:"days"`
Enabled bool `json:"enabled"`
// What happened the last time it fired, so a task that has been failing
// quietly for a week says so in the list rather than in a log nobody reads.
LastRun string `json:"lastRun,omitempty"` // RFC3339, UTC
LastResult string `json:"lastResult,omitempty"`
Created string `json:"created,omitempty"`
}
// ServiceRecord is one row of the Service log for a car.
type ServiceRecord struct {
ID string `json:"id"`