From 48c75875507aeec0a332df9a062b6a77255ad742 Mon Sep 17 00:00:00 2001 From: Thomas Klaehn Date: Thu, 11 Jun 2026 14:09:57 +0200 Subject: [PATCH] Fix: authorize by RFID or WebUI --- internal/api/charger_controller.go | 59 +++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/internal/api/charger_controller.go b/internal/api/charger_controller.go index 52b383a..57bcc0c 100644 --- a/internal/api/charger_controller.go +++ b/internal/api/charger_controller.go @@ -46,12 +46,29 @@ type ChargerController struct { } func NewChargerController(host string, pool *pgxpool.Pool) *ChargerController { - return &ChargerController{ + c := &ChargerController{ host: host, pool: pool, params: ChargingParams{Mode: ModeOff}, - status: "idle", + status: "initializing", } + // Always restore RFID-gated idle state on startup. nmo/acs settings persist + // on the charger hardware across app restarts, so we must reset them explicitly. + go c.initSafeState() + return c +} + +func (c *ChargerController) initSafeState() { + if err := setChargerFrc(c.host, 1); err != nil { + log.Printf("charger ctrl: init: %v", err) + c.setStatus("charger unreachable") + return + } + setChargerNmo(c.host, false) + setChargerAcs(c.host, 1) + setChargerFrc(c.host, 0) + c.setStatus("idle") + log.Printf("charger ctrl: initialized — RFID-gated (nmo=false, acs=1)") } func (c *ChargerController) State() ControllerState { @@ -131,12 +148,15 @@ func (c *ChargerController) enableCharging(amps int) error { if err != nil { return fmt.Errorf("charger unreachable: %w", err) } - if st.Car == 4 { - c.setStatus("resetting — previous session complete") + // car=4: previous session must be cleared before a new one can start. + // car=3: car entered IEC state B while acs=1 was active; the CP-line + // "wait for auth" signal means the car won't accept frc=2 alone — + // a full reset forces a fresh IEC 61851 handshake. + if st.Car == 4 || st.Car == 3 { + c.setStatus("resetting — fresh IEC negotiation needed") if err := resetCharger(c.host); err != nil { return fmt.Errorf("reset: %w", err) } - // Reset clears currentPhases so phase-switch logic starts fresh c.mu.Lock() c.currentPhases = 0 c.mu.Unlock() @@ -276,6 +296,24 @@ func (c *ChargerController) adjust(ctx context.Context) { return } } + + // frc=2 alone does not override acs=1 on this firmware: if access control + // is still blocking (alw=false) and we haven't intentionally force-paused + // (frc=1), the session hasn't been authorised yet — call enableCharging to + // set acs=0 and start the session properly. + // Guard on battPaused: if the battery SOC is already below the cutoff + // (set at the top of this function), do not enable charging even if the + // charger looks idle — the pause takes priority. + c.mu.RLock() + paused := c.battPaused + c.mu.RUnlock() + if !st.Alw && st.Frc != 1 && !paused { + if err := c.enableCharging(params.MaxAmp); err != nil { + log.Printf("charger ctrl: session start: %v", err) + c.setStatus("session start failed: " + err.Error()) + } + return + } } // Grid mode: current is fixed and already applied by enableCharging(); the @@ -412,7 +450,16 @@ func (c *ChargerController) stopOnDisconnect() { func (c *ChargerController) stopForTarget(reason string) { if err := setChargerFrc(c.host, 1); err != nil { - log.Printf("charger ctrl: stop: %v", err) + log.Printf("charger ctrl: stop frc1: %v", err) + } + if err := setChargerNmo(c.host, false); err != nil { + log.Printf("charger ctrl: stop nmo: %v", err) + } + if err := setChargerAcs(c.host, 1); err != nil { + log.Printf("charger ctrl: stop acs: %v", err) + } + if err := setChargerFrc(c.host, 0); err != nil { + log.Printf("charger ctrl: stop frc0: %v", err) } c.mu.Lock() c.params.Mode = ModeOff