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
This commit is contained in:
Andreas Linde 2023-04-08 21:55:07 +02:00 • committed by GitHub
parent 95b9cadc87
commit 4f4edba5ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 100 additions and 159 deletions

View file

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

View file

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

View file

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

4
go.mod
View file

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

8
go.sum
View file

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