Repository: evcc-io/evcc
Pull request #19284 (2025-03-01 10:52:49)

EEBus: use context
Fix https://github.com/evcc-io/evcc/issues


Patch:
diff --git a/charger/eebus.go b/charger/eebus.go
index f4589165..7cd12647 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"slices"
@@ -49,11 +50,11 @@ type EEBus struct {
 }
 
 func init() {
-	registry.Add("eebus", NewEEBusFromConfig)
+	registry.AddCtx("eebus", NewEEBusFromConfig)
 }
 
 // NewEEBusFromConfig creates an EEBus charger from generic config
-func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		Ski           string
 		Ip            string
@@ -68,13 +69,13 @@ func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEEBus(cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy, cc.VasVW)
+	return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy, cc.VasVW)
 }
 
 //go:generate go tool decorate -f decorateEEBus -b *EEBus -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.ChargeRater,ChargedEnergy,func() (float64, error)"
 
 // NewEEBus creates EEBus charger
-func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, error) {
+func NewEEBus(ctx context.Context, ski, ip string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, error) {
 	if eebus.Instance == nil {
 		return nil, errors.New("eebus not configured")
 	}
@@ -93,7 +94,7 @@ func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charg
 		return nil, err
 	}
 
-	if err := c.Wait(90 * time.Second); err != nil {
+	if err := c.Wait(ctx); err != nil {
 		return c, err
 	}
 
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:
diff --git a/hems/config.go b/hems/config.go
index c0843cff..d388a065 100644
--- a/hems/config.go
+++ b/hems/config.go
@@ -23,7 +23,7 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}
 	case "sma", "shm", "semp":
 		return semp.New(other, site, httpd)
 	case "eebus":
-		return eebus.New(other, site)
+		return eebus.New(ctx, other, site)
 	case "relay":
 		return relay.New(ctx, other, site)
 	default:
diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go
index 12f75515..8acb8032 100644
--- a/hems/eebus/eebus.go
+++ b/hems/eebus/eebus.go
@@ -1,6 +1,7 @@
 package eebus
 
 import (
+	"context"
 	"errors"
 	"sync"
 	"time"
@@ -40,7 +41,7 @@ type Limits struct {
 }
 
 // New creates an EEBus HEMS from generic config
-func New(other map[string]interface{}, site site.API) (*EEBus, error) {
+func New(ctx context.Context, other map[string]interface{}, site site.API) (*EEBus, error) {
 	cc := struct {
 		Ski    string
 		Limits `mapstructure:",squash"`
@@ -75,11 +76,11 @@ func New(other map[string]interface{}, site site.API) (*EEBus, error) {
 	}
 	site.SetCircuit(lpc)
 
-	return NewEEBus(cc.Ski, cc.Limits, lpc)
+	return NewEEBus(ctx, cc.Ski, cc.Limits, lpc)
 }
 
 // NewEEBus creates EEBus charger
-func NewEEBus(ski string, limits Limits, root api.Circuit) (*EEBus, error) {
+func NewEEBus(ctx context.Context, ski string, limits Limits, root api.Circuit) (*EEBus, error) {
 	if eebus.Instance == nil {
 		return nil, errors.New("eebus not configured")
 	}
@@ -104,7 +105,7 @@ func NewEEBus(ski string, limits Limits, root api.Circuit) (*EEBus, error) {
 		return nil, err
 	}
 
-	if err := c.Wait(90 * time.Second); err != nil {
+	if err := c.Wait(ctx); err != nil {
 		return c, err
 	}
 
diff --git a/meter/battery_power.go b/meter/battery_power.go
index 100cc5f5..c270b01d 100644
--- a/meter/battery_power.go
+++ b/meter/battery_power.go
@@ -4,6 +4,8 @@ type maxpower struct {
 	MaxACPower float64
 }
 
+// var _ api.MaxACPowerGetter = (*maxpower)(nil)
+
 // MaxACPowerDecorator returns the max AC power decorator
 func (m *maxpower) Decorator() func() float64 {
 	if m.MaxACPower == 0 {
diff --git a/meter/eebus.go b/meter/eebus.go
index 909130ce..f670caa7 100644
--- a/meter/eebus.go
+++ b/meter/eebus.go
@@ -1,6 +1,7 @@
 package meter
 
 import (
+	"context"
 	"errors"
 	"time"
 
@@ -23,11 +24,11 @@ type EEBus struct {
 }
 
 func init() {
-	registry.Add("eebus", NewEEBusFromConfig)
+	registry.AddCtx("eebus", NewEEBusFromConfig)
 }
 
 // New creates an EEBus HEMS from generic config
-func NewEEBusFromConfig(other map[string]interface{}) (api.Meter, error) {
+func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) {
 	cc := struct {
 		Ski     string
 		Ip      string
@@ -40,11 +41,11 @@ func NewEEBusFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, err
 	}
 
-	return NewEEBus(cc.Ski, cc.Ip, cc.Timeout)
+	return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Timeout)
 }
 
 // NewEEBus creates EEBus charger
-func NewEEBus(ski, ip string, timeout time.Duration) (*EEBus, error) {
+func NewEEBus(ctx context.Context, ski, ip string, timeout time.Duration) (*EEBus, error) {
 	if eebus.Instance == nil {
 		return nil, errors.New("eebus not configured")
 	}
@@ -63,7 +64,7 @@ func NewEEBus(ski, ip string, timeout time.Duration) (*EEBus, error) {
 		return nil, err
 	}
 
-	if err := c.Wait(90 * time.Second); err != nil {
+	if err := c.Wait(ctx); err != nil {
 		return c, err
 	}
 
diff --git a/server/eebus/connector.go b/server/eebus/connector.go
index e9f7dfe6..9ebf8c0b 100644
--- a/server/eebus/connector.go
+++ b/server/eebus/connector.go
@@ -1,12 +1,15 @@
 package eebus
 
 import (
+	"context"
 	"sync"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
 )
 
+const registerTimeout = 90 * time.Second
+
 type Connector struct {
 	once     sync.Once
 	connectC chan struct{}
@@ -16,9 +19,11 @@ func NewConnector() *Connector {
 	return &Connector{connectC: make(chan struct{})}
 }
 
-func (c *Connector) Wait(timeout time.Duration) error {
+func (c *Connector) Wait(ctx context.Context) error {
 	select {
-	case <-time.After(timeout):
+	case <-ctx.Done():
+		return ctx.Err()
+	case <-time.After(registerTimeout):
 		return api.ErrTimeout
 	case <-c.connectC:
 		return nil


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)
 }
diff --git a/meter/meter_test.go b/meter/meter_test.go
new file mode 100644
index 00000000..9316d3c8
--- /dev/null
+++ b/meter/meter_test.go
@@ -0,0 +1,30 @@
+package meter
+
+import (
+	"context"
+	"testing"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestACPower(t *testing.T) {
+	m, err := NewConfigurableFromConfig(context.TODO(), map[string]any{
+		"capacity": 23,
+		"soc": map[string]any{
+			"source": "const",
+			"value":  47,
+		},
+		"maxacpower": 1000,
+		"power": map[string]any{
+			"source": "const",
+			"value":  1000,
+		},
+	})
+	require.NoError(t, err)
+	_, ok := m.(api.BatteryCapacity)
+	assert.True(t, ok, "api.BatteryCapacity")
+	_, ok = m.(api.MaxACPowerGetter)
+	assert.True(t, ok, "api.MaxACPowerGetter")
+}