diff --git a/charger/ocpp.go b/charger/ocpp.go index 91c8f9ae2..597b13c06 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -29,6 +29,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api/implement" "github.com/evcc-io/evcc/charger/ocpp" + "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/sponsor" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" @@ -43,6 +44,7 @@ type OCPP struct { phases int enabled bool current float64 + lp loadpoint.API stackLevelZero bool profileKindRelative bool @@ -318,6 +320,14 @@ func (c *OCPP) createTxDefaultChargingProfile(current float64) *types.ChargingPr period := types.NewChargingSchedulePeriod(0, current) if c.cp.ChargingRateUnit == types.ChargingRateUnitWatts { + // c.phases is only set via the phase switcher; fall back to the loadpoint phases + if phases == 0 && c.lp != nil { + phases = c.lp.GetPhases() + } + // OCPP assumes phases == 3 if not set + if phases == 0 { + phases = 3 + } period = types.NewChargingSchedulePeriod(0, math.Trunc(230.0*current*float64(phases))) } else { // OCPP assumes phases == 3 if not set @@ -440,3 +450,10 @@ func (c *OCPP) Diagnose() { } } } + +var _ loadpoint.Controller = (*OCPP)(nil) + +// LoadpointControl implements loadpoint.Controller +func (c *OCPP) LoadpointControl(lp loadpoint.API) { + c.lp = lp +} diff --git a/charger/ocpp_phases_test.go b/charger/ocpp_phases_test.go new file mode 100644 index 000000000..d5af98f33 --- /dev/null +++ b/charger/ocpp_phases_test.go @@ -0,0 +1,49 @@ +package charger + +import ( + "testing" + + "github.com/evcc-io/evcc/charger/ocpp" + "github.com/evcc-io/evcc/core/loadpoint" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// TestWattsProfilePhases checks the watts power target falls back to loadpoint +// phases (then 3) when the phase switcher never set c.phases (issue #30998). +func TestWattsProfilePhases(t *testing.T) { + const current = 16.0 + + for _, tc := range []struct { + name string + phases int // c.phases (0 = phase switcher never called) + lpPhases int // loadpoint phases, -1 = no loadpoint + wantPhases int + }{ + {"switcher set", 3, -1, 3}, + {"from loadpoint 1p", 0, 1, 1}, + {"from loadpoint 3p", 0, 3, 3}, + {"no loadpoint fallback", 0, -1, 3}, + {"loadpoint unknown fallback", 0, 0, 3}, + } { + t.Run(tc.name, func(t *testing.T) { + c := &OCPP{ + cp: &ocpp.CP{ChargingRateUnit: types.ChargingRateUnitWatts}, + phases: tc.phases, + } + + if tc.lpPhases >= 0 { + ctrl := gomock.NewController(t) + lp := loadpoint.NewMockAPI(ctrl) + lp.EXPECT().GetPhases().Return(tc.lpPhases).AnyTimes() + c.lp = lp + } + + profile := c.createTxDefaultChargingProfile(current) + limit := profile.ChargingSchedule.ChargingSchedulePeriod[0].Limit + + require.Equal(t, 230.0*current*float64(tc.wantPhases), limit) + }) + } +}