From 5f5b33289c0701f622173070b82dd5b6738f527a Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 23 Aug 2026 11:32:22 +0200 Subject: [PATCH] OCPP: keep websocket alive between heartbeats (#33101) --- charger/ocpp/const.go | 8 ++++++++ charger/ocpp/cp_core.go | 2 +- charger/ocpp/cp_core_test.go | 3 +++ charger/ocpp/instance.go | 4 ++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/charger/ocpp/const.go b/charger/ocpp/const.go index ae20cf02d..893260f13 100644 --- a/charger/ocpp/const.go +++ b/charger/ocpp/const.go @@ -4,6 +4,14 @@ import "time" var Timeout = time.Minute // default request / response timeout on protocol level +const ( + heartbeatInterval = time.Minute // heartbeat interval requested in BootNotification + + // pingWait must exceed heartbeatInterval, otherwise chargers not sending + // websocket pings are disconnected while idle + pingWait = 3 * heartbeatInterval +) + // TriggerBootDelay defines how long to wait after WebSocket connect before // proactively triggering a BootNotification. This allows the connection to // stabilize and gives the charger a chance to send a spontaneous BootNotification. diff --git a/charger/ocpp/cp_core.go b/charger/ocpp/cp_core.go index d7f544100..9035b5632 100644 --- a/charger/ocpp/cp_core.go +++ b/charger/ocpp/cp_core.go @@ -16,7 +16,7 @@ var ( func (cp *CP) OnBootNotification(request *core.BootNotificationRequest) (*core.BootNotificationConfirmation, error) { res := &core.BootNotificationConfirmation{ CurrentTime: types.Now(), - Interval: 60, + Interval: int(heartbeatInterval.Seconds()), Status: core.RegistrationStatusAccepted, } diff --git a/charger/ocpp/cp_core_test.go b/charger/ocpp/cp_core_test.go index f6125690f..cfdc7eec0 100644 --- a/charger/ocpp/cp_core_test.go +++ b/charger/ocpp/cp_core_test.go @@ -28,6 +28,9 @@ func TestBootNotificationStoresResultAndConnects(t *testing.T) { require.NoError(t, err) assert.Equal(t, core.RegistrationStatusAccepted, res.Status) + // heartbeat must stay below the websocket inactivity timeout + assert.Less(t, time.Duration(res.Interval)*time.Second, pingWait) + // should be connected after BootNotification assert.True(t, cp.Connected(), "should be connected after BootNotification") assert.Equal(t, bootReq, cp.BootNotificationResult, "should store boot result") diff --git a/charger/ocpp/instance.go b/charger/ocpp/instance.go index 5d5351dca..2c5176d70 100644 --- a/charger/ocpp/instance.go +++ b/charger/ocpp/instance.go @@ -140,6 +140,10 @@ func NewServer(cfg Config, networkExternalUrl string) { server := &interceptingServer{Server: ws.NewServer()} server.SetCheckOriginHandler(func(r *http.Request) bool { return true }) + timeouts := ws.NewServerTimeoutConfig() + timeouts.PingWait = pingWait + server.SetTimeoutConfig(timeouts) + dispatcher := ocppj.NewDefaultServerDispatcher(ocppj.NewFIFOQueueMap(0)) endpoint := ocppj.NewServer(server, dispatcher, nil, core.Profile, remotetrigger.Profile, smartcharging.Profile, security.Profile, firmware.Profile)