Skip to content

Commit

Permalink
Loadpoint: sync phases only if switchable (#14690)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jul 4, 2024
1 parent f938588 commit f5adefe
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 16 deletions.
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"time"
)

//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit
//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit

// Meter provides total active power in W
type Meter interface {
Expand Down
80 changes: 78 additions & 2 deletions api/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 20 additions & 13 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,8 @@ func (lp *Loadpoint) syncCharger() error {
)

// use chargers actual set current if available
cg, ok := lp.charger.(api.CurrentGetter)
if ok {
cg, isCg := lp.charger.(api.CurrentGetter)
if isCg {
if current, err = cg.GetMaxCurrent(); err == nil {
// smallest adjustment most PWM-Controllers can do is: 100%÷256×0,6A = 0.234A
if math.Abs(lp.chargeCurrent-current) > 0.23 {
Expand All @@ -728,9 +728,9 @@ func (lp *Loadpoint) syncCharger() error {
}

// use measured phase currents as fallback if charger does not provide max current or does not currently relay from vehicle (TWC3)
if !ok || errors.Is(err, api.ErrNotAvailable) {
if !isCg || errors.Is(err, api.ErrNotAvailable) {
// validate if current too high by at least 1A
if current := lp.GetMaxPhaseCurrent(); current > lp.chargeCurrent+1.0 {
if current := lp.GetMaxPhaseCurrent(); current >= lp.chargeCurrent+1.0 {
if shouldBeConsistent {
lp.log.WARN.Printf("charger logic error: current mismatch (got %.3gA measured, expected %.3gA)", current, lp.chargeCurrent)
}
Expand All @@ -740,17 +740,21 @@ func (lp *Loadpoint) syncCharger() error {
}

// sync phases
phases := lp.GetPhases()
if shouldBeConsistent && phases > 0 {
// fallback active phases from measured phases
_, isPs := lp.charger.(api.PhaseSwitcher)
if phases := lp.GetPhases(); isPs && shouldBeConsistent && phases > 0 {
// fallback to active phases from measured phases
chargerPhases := lp.measuredPhases
if chargerPhases == 2 {
chargerPhases = 3
}

if pg, ok := lp.charger.(api.PhaseGetter); ok {
if cp, err := pg.GetPhases(); err == nil {
chargerPhases = cp
pg, isPg := lp.charger.(api.PhaseGetter)
if isPg {
if chargerPhases, err = pg.GetPhases(); err == nil {
if chargerPhases > 0 && chargerPhases != phases {
lp.log.WARN.Printf("charger logic error: phases mismatch (got %d, expected %d)", chargerPhases, phases)
lp.setPhases(chargerPhases)
}
} else {
if errors.Is(err, api.ErrNotAvailable) {
return nil
Expand All @@ -759,9 +763,12 @@ func (lp *Loadpoint) syncCharger() error {
}
}

if chargerPhases > 0 && chargerPhases != phases {
lp.log.WARN.Printf("charger logic error: phases mismatch (got %d, expected %d)", chargerPhases, phases)
lp.setPhases(chargerPhases)
// use measured phase currents for active phases as fallback if charger does not provide phases
if !isPg || errors.Is(err, api.ErrNotAvailable) {
if chargerPhases > phases {
lp.log.WARN.Printf("charger logic error: phases mismatch (got %d measured, expected %d)", chargerPhases, phases)
lp.setPhases(chargerPhases)
}
}
}

Expand Down
174 changes: 174 additions & 0 deletions core/loadpoint_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
import (
"testing"

evbus "github.com/asaskevich/EventBus"
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
Expand Down Expand Up @@ -47,3 +48,176 @@ func TestSyncCharger(t *testing.T) {
assert.Equal(t, tc.corrected, lp.enabled)
}
}

func TestSyncChargerCurrentsByGetter(t *testing.T) {
tc := []struct {
lpCurrent, actualCurrent, outCurrent float64
}{
{6, 5, 5}, // force
{6, 6.1, 6},
{6, 6.5, 6.5},
{6, 7, 7},
}

for _, tc := range tc {
ctrl := gomock.NewController(t)
t.Logf("%+v", tc)

ch := api.NewMockCharger(ctrl)
cg := api.NewMockCurrentGetter(ctrl)

charger := struct {
api.Charger
api.CurrentGetter
}{
ch, cg,
}

ch.EXPECT().Enabled().Return(true, nil)
cg.EXPECT().GetMaxCurrent().Return(tc.actualCurrent, nil).MaxTimes(1)

lp := &Loadpoint{
log: util.NewLogger("foo"),
bus: evbus.New(),
clock: clock.New(),
charger: charger,
status: api.StatusC,
enabled: true,
phases: 3,
chargeCurrent: tc.lpCurrent,
}

require.NoError(t, lp.syncCharger())
assert.Equal(t, tc.outCurrent, lp.chargeCurrent)
}
}

func TestSyncChargerCurrentsByMeasurement(t *testing.T) {
tc := []struct {
lpCurrent float64
chargeCurrents []float64
outCurrent float64
}{
{6, []float64{5, 0, 0}, 6}, // ignore
{6, []float64{6.1, 0, 0}, 6},
{6, []float64{6.5, 0, 0}, 6},
{6, []float64{7, 0, 0}, 7},
}

for _, tc := range tc {
ctrl := gomock.NewController(t)
t.Logf("%+v", tc)

charger := api.NewMockCharger(ctrl)
charger.EXPECT().Enabled().Return(true, nil)

lp := &Loadpoint{
log: util.NewLogger("foo"),
bus: evbus.New(),
clock: clock.New(),
charger: charger,
status: api.StatusC,
enabled: true,
phases: 3,
chargeCurrent: tc.lpCurrent,
chargeCurrents: tc.chargeCurrents,
}

require.NoError(t, lp.syncCharger())
assert.Equal(t, tc.outCurrent, lp.chargeCurrent)
}
}

func TestSyncChargerPhasesByGetter(t *testing.T) {
tc := []struct {
lpPhases, actualPhases, outPhases int
}{
{0, 0, 0},
{1, 0, 1},
{1, 1, 1},
{1, 3, 3},
{3, 0, 3},
{3, 1, 1}, // force
{3, 3, 3},
}

for _, tc := range tc {
ctrl := gomock.NewController(t)
t.Logf("%+v", tc)

ch := api.NewMockCharger(ctrl)
ps := api.NewMockPhaseSwitcher(ctrl)
pg := api.NewMockPhaseGetter(ctrl)

charger := struct {
api.Charger
api.PhaseSwitcher
api.PhaseGetter
}{
ch, ps, pg,
}

ch.EXPECT().Enabled().Return(true, nil)
pg.EXPECT().GetPhases().Return(tc.actualPhases, nil).MaxTimes(1)

lp := &Loadpoint{
log: util.NewLogger("foo"),
bus: evbus.New(),
clock: clock.New(),
charger: charger,
status: api.StatusC,
enabled: true,
phases: tc.lpPhases,
measuredPhases: tc.actualPhases,
}

require.NoError(t, lp.syncCharger())
assert.Equal(t, tc.outPhases, lp.phases)
}
}

func TestSyncChargerPhasesByMeasurement(t *testing.T) {
tc := []struct {
lpPhases, actualPhases, outPhases int
}{
{0, 0, 0},
{1, 0, 1},
{1, 1, 1},
{1, 3, 3},
{3, 0, 3},
{3, 1, 3}, // ignore
{3, 3, 3},
}

ctrl := gomock.NewController(t)

for _, tc := range tc {
t.Logf("%+v", tc)

ch := api.NewMockCharger(ctrl)
ps := api.NewMockPhaseSwitcher(ctrl)

charger := struct {
api.Charger
api.PhaseSwitcher
}{
ch, ps,
}

ch.EXPECT().Enabled().Return(true, nil)

lp := &Loadpoint{
log: util.NewLogger("foo"),
bus: evbus.New(),
clock: clock.New(),
charger: charger,
status: api.StatusC,
enabled: true,
phases: tc.lpPhases,
measuredPhases: tc.actualPhases,
}

require.NoError(t, lp.syncCharger())
assert.Equal(t, tc.outPhases, lp.phases)
}
}

0 comments on commit f5adefe

Please sign in to comment.