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:
tajniak81
2026-09-03 22:47:30 +02:00
co-authored by Claude Opus 5
parent 8e4c22cfcf
commit 5a4515978f
15 changed files with 1856 additions and 7 deletions
+16
View File
@@ -159,6 +159,7 @@ const (
colDocuments = "car_documents"
colReminders = "reminders"
colHomeChargers = "home_chargers"
colChargingTasks = "charging_tasks"
colControlAudit = "control_audit"
colAppSettings = "app_settings"
)
@@ -182,6 +183,10 @@ type Server struct {
ocpp *ocpp.CSMS
control *controlIndex // token -> owning user/charger for the /ocpp endpoint
ctlRL *rateLimiter // per user+charger control-command rate limit
// schedulerStop ends the ticker that fires the home-charger scheduler's
// tasks, started by StartScheduler (see chargingtasks_run.go).
schedulerStop context.CancelFunc
}
// New constructs a Server around an already-built PocketBase client.
@@ -285,6 +290,9 @@ func (s *Server) Stop(ctx context.Context) {
if s.pluginsStop != nil {
s.pluginsStop()
}
if s.schedulerStop != nil {
s.schedulerStop()
}
s.ocpp.Shutdown(ctx)
s.plugins.Shutdown(ctx)
}
@@ -488,6 +496,14 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("PATCH /api/home-chargers/{id}", s.updateHomeCharger)
mux.HandleFunc("DELETE /api/home-chargers/{id}", s.deleteHomeCharger)
// The home-charger scheduler: one list of charging tasks per user, covering
// every charger they own. See chargingtasks.go.
mux.HandleFunc("GET /api/charging-tasks", s.listChargingTasks)
mux.HandleFunc("POST /api/charging-tasks", s.createChargingTask)
mux.HandleFunc("PATCH /api/charging-tasks/{id}", s.updateChargingTask)
mux.HandleFunc("DELETE /api/charging-tasks/{id}", s.deleteChargingTask)
mux.HandleFunc("POST /api/charging-tasks/{id}/run", s.runChargingTaskNow)
// Cars + sharing.
mux.HandleFunc("GET /api/cars", s.listCars)
mux.HandleFunc("POST /api/cars", s.createCar)