Repository: evcc-io/evcc
Pull request #19282 (2025-03-01 10:45:09)

Ocpp: use context
Fix https://github.com/evcc-io/evcc/issues/19281

/cc @mfuchs1984 

Out of scope:

- [ ] closing the context will not stop an already initialised OCPP instance


Patch:
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 18907f46..15e25f5e 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -19,6 +19,7 @@ package charger
 
 import (
 	"cmp"
+	"context"
 	"fmt"
 	"math"
 	"slices"
@@ -50,11 +51,11 @@ type OCPP struct {
 const defaultIdTag = "evcc" // RemoteStartTransaction only
 
 func init() {
-	registry.Add("ocpp", NewOCPPFromConfig)
+	registry.AddCtx("ocpp", NewOCPPFromConfig)
 }
 
 // NewOCPPFromConfig creates a OCPP charger from generic config
-func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewOCPPFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		StationId      string
 		IdTag          string
@@ -84,7 +85,8 @@ func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) {
 
 	stackLevelZero := cc.StackLevelZero != nil && *cc.StackLevelZero
 
-	c, err := NewOCPP(cc.StationId, cc.Connector, cc.IdTag,
+	c, err := NewOCPP(ctx,
+		cc.StationId, cc.Connector, cc.IdTag,
 		cc.MeterValues, cc.MeterInterval,
 		stackLevelZero, cc.RemoteStart,
 		cc.ConnectTimeout)
@@ -137,7 +139,8 @@ func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) {
 //go:generate go tool decorate -f decorateOCPP -b *OCPP -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.CurrentGetter,GetMaxCurrent,func() (float64, error)" -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.Battery,Soc,func() (float64, error)"
 
 // NewOCPP creates OCPP charger
-func NewOCPP(id string, connector int, idTag string,
+func NewOCPP(ctx context.Context,
+	id string, connector int, idTag string,
 	meterValues string, meterInterval time.Duration,
 	stackLevelZero, remoteStart bool,
 	connectTimeout time.Duration,
@@ -154,12 +157,14 @@ func NewOCPP(id string, connector int, idTag string,
 			log.DEBUG.Printf("waiting for chargepoint: %v", connectTimeout)
 
 			select {
+			case <-ctx.Done():
+				return ctx.Err()
 			case <-time.After(connectTimeout):
 				return api.ErrTimeout
 			case <-cp.HasConnected():
 			}
 
-			return cp.Setup(meterValues, meterInterval)
+			return cp.Setup(ctx, meterValues, meterInterval)
 		},
 	)
 	if err != nil {
@@ -188,7 +193,9 @@ func NewOCPP(id string, connector int, idTag string,
 		stackLevelZero: stackLevelZero,
 	}
 
-	go conn.WatchDog(10 * time.Second)
+	if cp.HasRemoteTriggerFeature {
+		go conn.WatchDog(ctx, 10*time.Second)
+	}
 
 	return c, conn.Initialized()
 }
diff --git a/charger/ocpp/connector.go b/charger/ocpp/connector.go
index 43cbf7a8..258e5202 100644
--- a/charger/ocpp/connector.go
+++ b/charger/ocpp/connector.go
@@ -1,6 +1,7 @@
 package ocpp
 
 import (
+	"context"
 	"fmt"
 	"strconv"
 	"strings"
@@ -100,16 +101,22 @@ func (conn *Connector) GetScheduleLimit(duration int) (float64, error) {
 
 // WatchDog triggers meter values messages if older than timeout.
 // Must be wrapped in a goroutine.
-func (conn *Connector) WatchDog(timeout time.Duration) {
+func (conn *Connector) WatchDog(ctx context.Context, timeout time.Duration) {
 	tick := time.NewTicker(2 * time.Second)
-	for ; true; <-tick.C {
+	for {
 		conn.mu.Lock()
 		update := conn.clock.Since(conn.meterUpdated) > timeout
 		conn.mu.Unlock()
 
-		if update && conn.cp.HasRemoteTriggerFeature {
+		if update {
 			conn.TriggerMessageRequest(core.MeterValuesFeatureName)
 		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-tick.C:
+		}
 	}
 }
 
diff --git a/charger/ocpp/cp_setup.go b/charger/ocpp/cp_setup.go
index 1b999994..5f9d7d49 100644
--- a/charger/ocpp/cp_setup.go
+++ b/charger/ocpp/cp_setup.go
@@ -1,6 +1,7 @@
 package ocpp
 
 import (
+	"context"
 	"strconv"
 	"strings"
 	"time"
@@ -12,7 +13,7 @@ import (
 	"github.com/samber/lo"
 )
 
-func (cp *CP) Setup(meterValues string, meterInterval time.Duration) error {
+func (cp *CP) Setup(ctx context.Context, meterValues string, meterInterval time.Duration) error {
 	if err := cp.ChangeAvailabilityRequest(0, core.AvailabilityTypeOperative); err != nil {
 		cp.log.DEBUG.Printf("failed configuring availability: %v", err)
 	}
@@ -136,6 +137,8 @@ func (cp *CP) Setup(meterValues string, meterInterval time.Duration) error {
 		if err := cp.TriggerMessageRequest(0, core.MeterValuesFeatureName); err == nil {
 			// wait for meter values
 			select {
+			case <-ctx.Done():
+				return ctx.Err()
 			case <-time.After(Timeout):
 				cp.log.WARN.Println("meter timeout")
 			case <-cp.meterC:


Test patch:
diff --git a/charger/ocpp_test.go b/charger/ocpp_test.go
index 2339c388..4dbc72bd 100644
--- a/charger/ocpp_test.go
+++ b/charger/ocpp_test.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"errors"
 	"testing"
 	"time"
@@ -112,7 +113,7 @@ func (suite *ocppTestSuite) TestConnect() {
 	suite.Require().True(cp1.IsConnected())
 
 	// 1st charge point- local
-	c1, err := NewOCPP("test-1", 1, "", "", 0, false, true, ocppTestConnectTimeout)
+	c1, err := NewOCPP(context.TODO(), "test-1", 1, "", "", 0, false, true, ocppTestConnectTimeout)
 	suite.Require().NoError(err)
 
 	// status and meter values
@@ -161,7 +162,7 @@ func (suite *ocppTestSuite) TestConnect() {
 	suite.Require().True(cp2.IsConnected())
 
 	// 2nd charge point - local
-	c2, err := NewOCPP("test-2", 1, "", "", 0, false, true, ocppTestConnectTimeout)
+	c2, err := NewOCPP(context.TODO(), "test-2", 1, "", "", 0, false, true, ocppTestConnectTimeout)
 	suite.Require().NoError(err)
 
 	{
@@ -203,7 +204,7 @@ func (suite *ocppTestSuite) TestAutoStart() {
 	suite.Require().True(cp1.IsConnected())
 
 	// 1st charge point- local
-	c1, err := NewOCPP("test-3", 1, "", "", 0, false, false, ocppTestConnectTimeout)
+	c1, err := NewOCPP(context.TODO(), "test-3", 1, "", "", 0, false, false, ocppTestConnectTimeout)
 	suite.Require().NoError(err)
 
 	// status and meter values
@@ -250,7 +251,7 @@ func (suite *ocppTestSuite) TestTimeout() {
 	})
 
 	// 1st charge point- local
-	_, err := NewOCPP("test-4", 1, "", "", 0, false, false, ocppTestConnectTimeout)
+	_, err := NewOCPP(context.TODO(), "test-4", 1, "", "", 0, false, false, ocppTestConnectTimeout)
 
 	suite.Require().NoError(err)
 }