From 4f4edba5bab4529ddec2645a50b30dbc4f6937eb Mon Sep 17 00:00:00 2001 From: Andreas Linde <42185+DerAndereAndi@users.noreply.github.com> Date: Sat, 8 Apr 2023 21:55:07 +0200 Subject: [PATCH] EEBUS updates and fixes (#7173) * Adopt EEBUS v0.2 dev changes * Fix filling up currents * Improve enabled state handling when unplugged * Use ErrTimeout if disconnected --- charger/eebus.go | 113 +++++++++++++++++-------------------- charger/eebus/eebus.go | 9 ++- charger/eebus_test.go | 125 ++++++++++++----------------------------- go.mod | 4 +- go.sum | 8 +-- 5 files changed, 100 insertions(+), 159 deletions(-) diff --git a/charger/eebus.go b/charger/eebus.go index cc958b749..0887fa564 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -29,8 +29,8 @@ type EEBus struct { communicationStandard emobility.EVCommunicationStandardType - maxCurrent float64 - expectedEnableState bool + expectedEnableUnpluggedState bool + current float64 lastIsChargingCheck time.Time lastIsChargingResult bool @@ -79,9 +79,10 @@ func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy bool) (api.Charger, err log: log, connectedC: make(chan bool, 1), communicationStandard: emobility.EVCommunicationStandardTypeUnknown, + current: 6, } - c.emobility = eebus.Instance.Register(ski, ip, c.onConnect, c.onDisconnect) + c.emobility = eebus.Instance.RegisterEVSE(ski, ip, c.onConnect, c.onDisconnect, nil) err := c.waitForConnection() @@ -114,6 +115,7 @@ func (c *EEBus) waitForConnection() error { func (c *EEBus) onConnect(ski string) { c.log.TRACE.Println("!! onConnect invoked on ski ", ski) + c.expectedEnableUnpluggedState = false c.setDefaultValues() c.setConnected(true) } @@ -121,13 +123,12 @@ func (c *EEBus) onConnect(ski string) { func (c *EEBus) onDisconnect(ski string) { c.log.TRACE.Println("!! onDisconnect invoked on ski ", ski) - c.expectedEnableState = false + c.expectedEnableUnpluggedState = false c.setConnected(false) c.setDefaultValues() } func (c *EEBus) setDefaultValues() { - c.expectedEnableState = false c.communicationStandard = emobility.EVCommunicationStandardTypeUnknown c.lastIsChargingCheck = time.Now().Add(-time.Hour * 1) c.lastIsChargingResult = false @@ -227,32 +228,30 @@ func (c *EEBus) updateState() (api.ChargeStatus, error) { } if !c.isConnected() { - return api.StatusNone, fmt.Errorf("%s disconnected", c.ski) + return api.StatusNone, api.ErrTimeout } switch currentState { case emobility.EVChargeStateTypeUnknown, emobility.EVChargeStateTypeUnplugged: // Unplugged - c.expectedEnableState = false + c.expectedEnableUnpluggedState = false return api.StatusA, nil case emobility.EVChargeStateTypeFinished, emobility.EVChargeStateTypePaused: // Finished, Paused return api.StatusB, nil case emobility.EVChargeStateTypeActive: // Active if c.isCharging() { - // we might already be enabled and charging due to connection issues - c.expectedEnableState = true return api.StatusC, nil } return api.StatusB, nil case emobility.EVChargeStateTypeError: // Error return api.StatusF, nil + default: + return api.StatusNone, fmt.Errorf("%s properties unknown result: %s", c.ski, currentState) } - - return api.StatusNone, fmt.Errorf("%s properties unknown result: %s", c.ski, currentState) } // Status implements the api.Charger interface func (c *EEBus) Status() (api.ChargeStatus, error) { - // check the current limits and update if necesarry + // check the current limits and update if necessary c.setLoadpointMinMaxLimits() return c.updateState() @@ -261,63 +260,55 @@ func (c *EEBus) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface // should return true if the charger allows the EV to draw power func (c *EEBus) Enabled() (bool, error) { - _, err := c.updateState() - return c.expectedEnableState, err + // when unplugged there is no overload limit data available + state, err := c.updateState() + if err != nil || state == api.StatusA { + return c.expectedEnableUnpluggedState, nil + } + + limits, err := c.emobility.EVLoadControlObligationLimits() + if err != nil { + // there are no overload protection limits available, e.g. because the data was not received yet + return true, nil + } + + for _, limit := range limits { + // for IEC61851 the pause limit is 0A, for ISO15118-2 it is 0.1A + // instead of checking for the actual data, hardcode this, so we might run into less + // timing issues as the data might not be received yet + if limit >= 1 { + return true, nil + } + } + + return false, nil } // Enable implements the api.Charger interface func (c *EEBus) Enable(enable bool) error { - currentState, err := c.emobility.EVCurrentChargeState() - if err != nil || currentState == emobility.EVChargeStateTypeUnplugged { - // if the ev is unplugged, we do not need to disable charging by setting a current of 0 as it already is - if !enable { - return nil - } - // if the ev is unplugged, we can not enable charging - return errors.New("can not enable charging as ev is unplugged") + // if the ev is unplugged or the state is unknown, there is nothing to be done + state, err := c.updateState() + if err != nil || state == api.StatusA { + c.expectedEnableUnpluggedState = enable + return nil } // if we disable charging with a potential but not yet known communication standard ISO15118 // this would set allowed A value to be 0. And this would trigger ISO connections to switch to IEC! - comStandard, err := c.emobility.EVCommunicationStandard() - if err != nil || comStandard == emobility.EVCommunicationStandardTypeUnknown { - return api.ErrMustRetry - } - - // we have to know the limits - minLimits, maxLimits, _, err := c.emobility.EVCurrentLimits() - if err != nil { - return api.ErrMustRetry - } - - c.expectedEnableState = enable - if !enable { - // Important notes on enabling/disabling!! - // ISO15118 mode: - // non-asymmetric or all phases set to 0: the OBC will wait for 1 minute, if the values remain after 1 min, it will pause then - // asymmetric and only some phases set to 0: no pauses or waiting for changes required - // asymmetric mode requires Plug & Charge (PnC) and Value Added Services (VAS) - // IEC61851 mode: - // switching between 1/3 phases: stop charging, pause for 2 minutes, change phases, resume charging - // frequent switching should be avoided by all means! - c.maxCurrent = 0 - return c.writeCurrentLimitData([]float64{0, 0, 0}) + comStandard, err := c.emobility.EVCommunicationStandard() + if err != nil || comStandard == emobility.EVCommunicationStandardTypeUnknown { + return api.ErrMustRetry + } } - // if we set MaxCurrent > Min value and then try to enable the charger, it would reset it to min - if c.maxCurrent > 0 { - return c.writeCurrentLimitData([]float64{c.maxCurrent, c.maxCurrent, c.maxCurrent}) + var current float64 + + if enable { + current = c.current } - // we need to check if the mode is set to now as the currents won't be adjusted afterwards any more in all cases - if c.lp.GetMode() == api.ModeNow { - return c.writeCurrentLimitData(maxLimits) - } - - // in non now mode only enable with min settings, so we don't excessively consume power in case it has to be turned of in the next cycle anyways - - return c.writeCurrentLimitData(minLimits) + return c.writeCurrentLimitData([]float64{current, current, current}) } // send current charging power limits to the EV @@ -366,10 +357,12 @@ func (c *EEBus) MaxCurrentMillis(current float64) error { return errors.New("can't set new current as ev is unplugged") } - c.maxCurrent = current + err = c.writeCurrentLimitData([]float64{current, current, current}) + if err == nil { + c.current = current + } - currents := []float64{current, current, current} - return c.writeCurrentLimitData(currents) + return err } // CurrentPower implements the api.Meter interface @@ -441,7 +434,7 @@ func (c *EEBus) currents() (float64, float64, float64, error) { count := len(currents) if count < 3 { - for fill := 0; fill < count-3; fill++ { + for fill := 0; fill < 3-count; fill++ { currents = append(currents, 0) } } diff --git a/charger/eebus/eebus.go b/charger/eebus/eebus.go index 56489c95d..3f5b249a6 100644 --- a/charger/eebus/eebus.go +++ b/charger/eebus/eebus.go @@ -117,14 +117,17 @@ func NewServer(other map[string]interface{}) (*EEBus, error) { } c.Cem = cem.NewCEM(configuration, c, c) - if err := c.Cem.Setup(true); err != nil { + if err := c.Cem.Setup(); err != nil { return nil, err } + c.Cem.EnableEmobility(emobility.EmobilityConfiguration{ + CoordinatedChargingEnabled: false, + }) return c, nil } -func (c *EEBus) Register(ski, ip string, connectHandler func(string), disconnectHandler func(string)) *emobility.EMobilityImpl { +func (c *EEBus) RegisterEVSE(ski, ip string, connectHandler func(string), disconnectHandler func(string), dataProvider emobility.EmobilityDataProvider) *emobility.EMobilityImpl { ski = strings.ReplaceAll(ski, "-", "") ski = strings.ReplaceAll(ski, " ", "") ski = strings.ToLower(ski) @@ -141,7 +144,7 @@ func (c *EEBus) Register(ski, ip string, connectHandler func(string), disconnect defer c.mux.Unlock() c.clients[ski] = EEBusClientCBs{onConnect: connectHandler, onDisconnect: disconnectHandler} - return c.Cem.RegisterEmobilityRemoteDevice(serviceDetails) + return c.Cem.RegisterEmobilityRemoteDevice(serviceDetails, dataProvider) } func (c *EEBus) Run() { diff --git a/charger/eebus_test.go b/charger/eebus_test.go index b0ff70c42..687a2f8f7 100644 --- a/charger/eebus_test.go +++ b/charger/eebus_test.go @@ -4,85 +4,25 @@ import ( "testing" "github.com/enbility/cemd/emobility" + "github.com/golang/mock/gomock" ) -type limitStruct struct { - phase uint - min, max, pause float64 -} - -type measurementStruct struct { - phase uint - current float64 -} - -type testMeasurementStruct struct { - expected bool - data []measurementStruct -} - -// Emobility mock - -type EmobilityMock struct { - connectedPhases uint - currents, limitsMin, limitsMax, limitsDefault []float64 -} - -func (e *EmobilityMock) EVCurrentChargeState() (emobility.EVChargeStateType, error) { - return emobility.EVChargeStateTypeUnknown, nil -} - -func (e *EmobilityMock) EVConnectedPhases() (uint, error) { - return e.connectedPhases, nil -} - -func (e *EmobilityMock) EVChargedEnergy() (float64, error) { - return 0, nil -} - -func (e *EmobilityMock) EVPowerPerPhase() ([]float64, error) { - return []float64{}, nil -} - -func (e *EmobilityMock) EVCurrentsPerPhase() ([]float64, error) { - return e.currents, nil -} - -func (e *EmobilityMock) EVCurrentLimits() ([]float64, []float64, []float64, error) { - return e.limitsMin, e.limitsMax, e.limitsDefault, nil -} - -func (e *EmobilityMock) EVWriteLoadControlLimits(obligations, recommendations []float64) error { - return nil -} - -func (e *EmobilityMock) EVCommunicationStandard() (emobility.EVCommunicationStandardType, error) { - return emobility.EVCommunicationStandardTypeUnknown, nil -} - -func (e *EmobilityMock) EVIdentification() (string, error) { - return "", nil -} - -func (e *EmobilityMock) EVOptimizationOfSelfConsumptionSupported() (bool, error) { - return false, nil -} - -func (e *EmobilityMock) EVSoCSupported() (bool, error) { - return false, nil -} - -func (e *EmobilityMock) EVSoC() (float64, error) { - return 0, nil -} - -func (e *EmobilityMock) EVCoordinatedChargingSupported() (bool, error) { - return false, nil -} - -var _ emobility.EmobilityI = (*EmobilityMock)(nil) - func TestEEBusIsCharging(t *testing.T) { + type limitStruct struct { + phase uint + min, max, pause float64 + } + + type measurementStruct struct { + phase uint + current float64 + } + + type testMeasurementStruct struct { + expected bool + data []measurementStruct + } + tests := []struct { name string limits []limitStruct @@ -162,35 +102,40 @@ func TestEEBusIsCharging(t *testing.T) { }, } - emobilityMock := &EmobilityMock{} - eebus := &EEBus{ - emobility: emobilityMock, - } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - emobilityMock.connectedPhases = 3 - emobilityMock.limitsMin = make([]float64, 0) - emobilityMock.limitsMax = make([]float64, 0) - emobilityMock.limitsDefault = make([]float64, 0) + limitsMin := make([]float64, 0) + limitsMax := make([]float64, 0) + limitsDefault := make([]float64, 0) for _, limit := range tc.limits { - emobilityMock.limitsMin = append(emobilityMock.limitsMin, limit.min) - emobilityMock.limitsMax = append(emobilityMock.limitsMax, limit.max) - emobilityMock.limitsDefault = append(emobilityMock.limitsDefault, limit.pause) + limitsMin = append(limitsMin, limit.min) + limitsMax = append(limitsMax, limit.max) + limitsDefault = append(limitsDefault, limit.pause) } for index, m := range tc.measurements { - emobilityMock.currents = make([]float64, 0) + ctrl := gomock.NewController(t) + + emobilityMock := emobility.NewMockEmobilityI(ctrl) + eebus := &EEBus{ + emobility: emobilityMock, + } + + currents := make([]float64, 0) for _, d := range m.data { - emobilityMock.currents = append(emobilityMock.currents, d.current) + currents = append(currents, d.current) } + emobilityMock.EXPECT().EVCurrentsPerPhase().Return(currents, nil).AnyTimes() + emobilityMock.EXPECT().EVCurrentLimits().Return(limitsMin, limitsMax, limitsDefault, nil) + result := eebus.isCharging() if result != m.expected { t.Errorf("Failure: test %s, series %d, expected %v, got %v", tc.name, index, m.expected, result) } + ctrl.Finish() } }) } diff --git a/go.mod b/go.mod index 82890cfe8..88b2555e8 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/dylanmei/iso8601 v0.1.0 github.com/eclipse/paho.mqtt.golang v1.4.2 - github.com/enbility/cemd v0.1.7 - github.com/enbility/eebus-go v0.1.7 + github.com/enbility/cemd v0.2.0 + github.com/enbility/eebus-go v0.2.0 github.com/fatih/structs v1.1.0 github.com/foogod/go-powerwall v0.2.0 github.com/glebarez/sqlite v1.7.0 diff --git a/go.sum b/go.sum index de6a578bc..d52f14ce7 100644 --- a/go.sum +++ b/go.sum @@ -346,10 +346,10 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/enbility/cemd v0.1.7 h1:PuofIXdILw3rXe1JMWHW7bA2ritaGp/3wy8rBRC8png= -github.com/enbility/cemd v0.1.7/go.mod h1:yjDruIQ9LkQI9dEvmYcp6y0QeVRPC9P8tli3Ehp8Ecs= -github.com/enbility/eebus-go v0.1.7 h1:iPOp3oJ12cExOfS9tcBbsHRSOyr+uKGypjAjRP8SdaE= -github.com/enbility/eebus-go v0.1.7/go.mod h1:Ozg1eDUfSbHfQ1dWfyAUa3h8dMtgM/01eO30kHca5zk= +github.com/enbility/cemd v0.2.0 h1:T+XbqX2PYKmbIgtdNAtKk1DVZaXjOAsIt+NqlIs4Ajo= +github.com/enbility/cemd v0.2.0/go.mod h1:BZoHbJQJ9/7le4WMFAJWRSgKCCTfNVEOM0c1E3H1JxE= +github.com/enbility/eebus-go v0.2.0 h1:znQUfG1QYk0Q+vOacrsSNtXmitF1F2Rx9+ohwcRNlRw= +github.com/enbility/eebus-go v0.2.0/go.mod h1:Ozg1eDUfSbHfQ1dWfyAUa3h8dMtgM/01eO30kHca5zk= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=