Loadpoint: sync phases only if switchable (#14690)
Some checks failed
Release / call-build-workflow (push) Has been cancelled
Release / Publish Docker :release (push) Has been cancelled
Release / Github & APT (push) Has been cancelled
Release / Demo (push) Has been cancelled
Release / Hassio Addon (push) Has been cancelled

This commit is contained in:
andig 2024-07-04 19:50:24 +02:00 • committed by GitHub
parent f938588c50
commit f5adefe753
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 273 additions and 16 deletions

View file

@ -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 {

View file

@ -1,9 +1,9 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit)
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit)
//
// Generated by this command:
//
// 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
// 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
//
// Package api is a generated GoMock package.
@ -173,6 +173,44 @@ func (mr *MockCurrentLimiterMockRecorder) GetMinMaxCurrent() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinMaxCurrent", reflect.TypeOf((*MockCurrentLimiter)(nil).GetMinMaxCurrent))
}
// MockCurrentGetter is a mock of CurrentGetter interface.
type MockCurrentGetter struct {
ctrl *gomock.Controller
recorder *MockCurrentGetterMockRecorder
}
// MockCurrentGetterMockRecorder is the mock recorder for MockCurrentGetter.
type MockCurrentGetterMockRecorder struct {
mock *MockCurrentGetter
}
// NewMockCurrentGetter creates a new mock instance.
func NewMockCurrentGetter(ctrl *gomock.Controller) *MockCurrentGetter {
mock := &MockCurrentGetter{ctrl: ctrl}
mock.recorder = &MockCurrentGetterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCurrentGetter) EXPECT() *MockCurrentGetterMockRecorder {
return m.recorder
}
// GetMaxCurrent mocks base method.
func (m *MockCurrentGetter) GetMaxCurrent() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMaxCurrent")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMaxCurrent indicates an expected call of GetMaxCurrent.
func (mr *MockCurrentGetterMockRecorder) GetMaxCurrent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockCurrentGetter)(nil).GetMaxCurrent))
}
// MockPhaseSwitcher is a mock of PhaseSwitcher interface.
type MockPhaseSwitcher struct {
ctrl *gomock.Controller
@ -210,6 +248,44 @@ func (mr *MockPhaseSwitcherMockRecorder) Phases1p3p(arg0 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases1p3p", reflect.TypeOf((*MockPhaseSwitcher)(nil).Phases1p3p), arg0)
}
// MockPhaseGetter is a mock of PhaseGetter interface.
type MockPhaseGetter struct {
ctrl *gomock.Controller
recorder *MockPhaseGetterMockRecorder
}
// MockPhaseGetterMockRecorder is the mock recorder for MockPhaseGetter.
type MockPhaseGetterMockRecorder struct {
mock *MockPhaseGetter
}
// NewMockPhaseGetter creates a new mock instance.
func NewMockPhaseGetter(ctrl *gomock.Controller) *MockPhaseGetter {
mock := &MockPhaseGetter{ctrl: ctrl}
mock.recorder = &MockPhaseGetterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPhaseGetter) EXPECT() *MockPhaseGetterMockRecorder {
return m.recorder
}
// GetPhases mocks base method.
func (m *MockPhaseGetter) GetPhases() (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPhases")
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPhases indicates an expected call of GetPhases.
func (mr *MockPhaseGetterMockRecorder) GetPhases() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPhases", reflect.TypeOf((*MockPhaseGetter)(nil).GetPhases))
}
// MockIdentifier is a mock of Identifier interface.
type MockIdentifier struct {
ctrl *gomock.Controller

View file

@ -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 {
@ -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)
}
@ -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
@ -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)
}
}
}

View file

@ -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"
@ -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)
}
}