diff --git a/charger/eebus.go b/charger/eebus.go index 76fef4e85..19d9d8fb4 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -17,12 +17,12 @@ import ( "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/server/eebus" "github.com/evcc-io/evcc/util" + "github.com/samber/lo" ) const ( - maxIdRequestTimespan = time.Second * 120 - idleFactor = 0.6 - voltage float64 = 230 + idleFactor = 0.6 + voltage float64 = 230 ) type minMax struct { @@ -38,17 +38,12 @@ type EEBus struct { lp loadpoint.API minMaxG func() (minMax, error) - communicationStandard model.DeviceConfigurationKeyValueStringType - vasVW bool // wether the EVSE supports VW VAS with ISO15118-2 - - expectedEnableUnpluggedState bool - reconnect bool - current float64 - - currentLimit float64 + vasVW bool // wether the EVSE supports VW VAS with ISO15118-2 + enabled bool + reconnect bool + current float64 *eebus.Connector - connectedTime time.Time } func init() { @@ -89,7 +84,7 @@ func NewEEBus(ski string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, uc: eebus.Instance.Evse(), } - c.Connector = eebus.NewConnector(c.connectEvent) + c.Connector = eebus.NewConnector() c.minMaxG = provider.Cached(c.minMax, time.Second) if err := eebus.Instance.RegisterDevice(ski, c); err != nil { @@ -111,23 +106,6 @@ func NewEEBus(ski string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, return c, nil } -func (c *EEBus) evEntity() spineapi.EntityRemoteInterface { - c.mux.RLock() - defer c.mux.RUnlock() - - return c.ev -} - -func (c *EEBus) connectEvent(connected bool) { - if connected && !c.Connected() { - c.mux.Lock() - c.connectedTime = time.Now() - c.mux.Unlock() - } - - c.setDefaultValues() -} - var _ eebus.Device = (*EEBus)(nil) // UseCaseEvent implements the eebus.Device interface @@ -140,54 +118,21 @@ func (c *EEBus) UseCaseEvent(device spineapi.DeviceRemoteInterface, entity spine case evcc.EvConnected: c.ev = entity c.reconnect = true - c.currentLimit = -1 case evcc.EvDisconnected: c.ev = nil - c.currentLimit = -1 } } -func (c *EEBus) setDefaultValues() { - c.communicationStandard = evcc.EVCCCommunicationStandardUnknown - c.expectedEnableUnpluggedState = false -} +func (c *EEBus) isEvConnected() (spineapi.EntityRemoteInterface, bool) { + c.mux.RLock() + defer c.mux.RUnlock() -var _ api.CurrentLimiter = (*EEBus)(nil) - -func (c *EEBus) minMax() (minMax, error) { - evEntity := c.evEntity() - if !c.uc.EvCC.EVConnected(evEntity) { - return minMax{}, errors.New("no ev connected") - } - - minLimits, maxLimits, _, err := c.uc.OpEV.CurrentLimits(evEntity) - if err != nil { - if err == eebusapi.ErrDataNotAvailable { - err = api.ErrNotAvailable - } - return minMax{}, err - } - - if len(minLimits) == 0 || len(maxLimits) == 0 { - return minMax{}, api.ErrNotAvailable - } - - return minMax{minLimits[0], maxLimits[0]}, nil -} - -func (c *EEBus) GetMinMaxCurrent() (float64, float64, error) { - minMax, err := c.minMaxG() - return minMax.min, minMax.max, err + return c.ev, c.ev != nil && c.uc.EvCC.EVConnected(c.ev) } // we assume that if any phase current value is > idleFactor * min Current, then charging is active and enabled is true -func (c *EEBus) isCharging() bool { - evEntity := c.evEntity() - if !c.uc.EvCC.EVConnected(evEntity) { - return false - } - +func (c *EEBus) isCharging(evEntity spineapi.EntityRemoteInterface) bool { // check if an external physical meter is assigned // we only want this for configured meters and not for internal meters! // right now it works as expected @@ -202,62 +147,65 @@ func (c *EEBus) isCharging() bool { if err != nil { return false } - limitsMin, _, _, err := c.uc.OpEV.CurrentLimits(evEntity) - if err != nil || limitsMin == nil || len(limitsMin) == 0 { - return false - } - var phasesCurrent float64 - for _, phaseCurrent := range currents { - phasesCurrent += phaseCurrent + limitsMin, _, _, err := c.uc.OpEV.CurrentLimits(evEntity) + if err != nil || len(limitsMin) == 0 { + // sometimes a min limit is not provided by the EVSE, so take the limit defined for the loadpoint + if c.lp == nil { + return false + } + limitsMin = []float64{c.lp.GetMinCurrent()} } // require sum of all phase currents to be > 0.6 * a single phase minimum // in some scenarios, e.g. Cayenne Hybrid, sometimes the meter of a PMCC device // reported 600W, even tough the car was not charging limitMin := limitsMin[0] - return phasesCurrent > limitMin*idleFactor + return lo.Sum(currents) > limitMin*idleFactor } // Status implements the api.Charger interface func (c *EEBus) Status() (res api.ChargeStatus, err error) { - if !c.Connected() { - return api.StatusNone, api.ErrTimeout + evEntity, ok := c.isEvConnected() + if !ok { + return api.StatusA, nil } // re-set current limit after reconnect defer func() { - if err == nil { - c.mux.Lock() - if c.reconnect { - c.reconnect = false - c.mux.Unlock() - err = c.MaxCurrentMillis(c.current) - } else { - c.mux.Unlock() - } + if err != nil { + return } - }() - evEntity := c.evEntity() - if !c.uc.EvCC.EVConnected(evEntity) { - c.expectedEnableUnpluggedState = false - return api.StatusA, nil - } + c.mux.Lock() + if !c.reconnect && (res == api.StatusB || res == api.StatusC) { + c.mux.Unlock() + return + } + + c.reconnect = false + c.mux.Unlock() + + var current float64 + if c.enabled { + current = c.current + } + + err = c.writeCurrentLimitData(evEntity, current) + }() currentState, err := c.uc.EvCC.ChargeState(evEntity) if err != nil { - return api.StatusNone, err + return api.StatusA, nil } switch currentState { case ucapi.EVChargeStateTypeUnknown, ucapi.EVChargeStateTypeUnplugged: // Unplugged - c.expectedEnableUnpluggedState = false return api.StatusA, nil case ucapi.EVChargeStateTypeFinished, ucapi.EVChargeStateTypePaused: // Finished, Paused return api.StatusB, nil case ucapi.EVChargeStateTypeActive: // Active - if c.isCharging() { + if c.isCharging(evEntity) { return api.StatusC, nil } return api.StatusB, nil @@ -272,32 +220,24 @@ func (c *EEBus) Status() (res api.ChargeStatus, err error) { // should return true if the charger allows the EV to draw power func (c *EEBus) Enabled() (bool, error) { // when unplugged there is no overload limit data available - evEntity := c.evEntity() - state, err := c.Status() - if err != nil || state == api.StatusA || evEntity == nil { - c.log.DEBUG.Println("!! EV unplugged or status unknown") - return c.expectedEnableUnpluggedState, nil - } - - // if the EV is charging - if state == api.StatusC { - c.log.DEBUG.Println("!! api.StatusC") - return true, nil + evEntity, ok := c.isEvConnected() + if !ok { + return c.enabled, nil } // if the VW VAS PV mode is active, use PV limits - if c.hasActiveVASVW() { + if c.hasActiveVASVW(evEntity) { limits, err := c.uc.OscEV.LoadControlLimits(evEntity) if err != nil { - c.log.DEBUG.Println("!! OscEV.LoadControlLimits error", err) // there are no limits available, e.g. because the data was not received yet - return true, nil + c.log.ERROR.Println("!! OscEV.LoadControlLimits:", err) + return c.enabled, nil } for _, limit := range limits { // check if there is an active limit set if limit.IsActive && limit.Value >= 1 { - c.log.DEBUG.Println("!! OscEV.LoadControlLimits active:", limit) + c.log.DEBUG.Println("!! OscEV.LoadControlLimits set:", limit) return true, nil } } @@ -308,8 +248,8 @@ func (c *EEBus) Enabled() (bool, error) { limits, err := c.uc.OpEV.LoadControlLimits(evEntity) if err != nil { // there are no limits available, e.g. because the data was not received yet - c.log.DEBUG.Println("!! OpEV.LoadControlLimits error:", err) - return true, nil + c.log.ERROR.Println("!! OpEV.LoadControlLimits:", err) + return c.enabled, nil } for _, limit := range limits { @@ -317,8 +257,7 @@ func (c *EEBus) Enabled() (bool, error) { // 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 the limit is not active, then the maximum possible current is permitted - if (limit.IsActive && limit.Value >= 1) || - !limit.IsActive { + if limit.IsActive && limit.Value >= 1 || !limit.IsActive { c.log.DEBUG.Println("!! OpEV.LoadControlLimits set:", limit) return true, nil } @@ -330,36 +269,36 @@ func (c *EEBus) Enabled() (bool, error) { // Enable implements the api.Charger interface func (c *EEBus) Enable(enable bool) error { // if the ev is unplugged or the state is unknown, there is nothing to be done - if state, err := c.Status(); err != nil || state == api.StatusA { - c.expectedEnableUnpluggedState = enable + evEntity, ok := c.isEvConnected() + if !ok { + c.enabled = 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! if !enable { - comStandard, err := c.uc.EvCC.CommunicationStandard(c.evEntity()) + comStandard, err := c.uc.EvCC.CommunicationStandard(evEntity) if err != nil || comStandard == evcc.EVCCCommunicationStandardUnknown { return api.ErrMustRetry } } var current float64 - if enable { current = c.current } - return c.writeCurrentLimitData([]float64{current, current, current}) + err := c.writeCurrentLimitData(evEntity, current) + if err == nil { + c.enabled = enable + } + + return err } // send current charging power limits to the EV -func (c *EEBus) writeCurrentLimitData(currents []float64) error { - evEntity := c.evEntity() - if !c.uc.EvCC.EVConnected(evEntity) { - return errors.New("no ev connected") - } - +func (c *EEBus) writeCurrentLimitData(evEntity spineapi.EntityRemoteInterface, current float64) error { // check if the EVSE supports overload protection limits if !c.uc.OpEV.IsScenarioAvailableAtEntity(evEntity, 1) { return api.ErrNotAvailable @@ -371,12 +310,8 @@ func (c *EEBus) writeCurrentLimitData(currents []float64) error { } // setup the limit data structure - limits := []ucapi.LoadLimitsPhase{} - for phase, current := range currents { - if phase >= len(ucapi.PhaseNameMapping) { - continue - } - + var limits []ucapi.LoadLimitsPhase + for phase := range len(ucapi.PhaseNameMapping) { limit := ucapi.LoadLimitsPhase{ Phase: ucapi.PhaseNameMapping[phase], IsActive: true, @@ -393,47 +328,29 @@ func (c *EEBus) writeCurrentLimitData(currents []float64) error { // if VAS VW is available, limits are completely covered by it // this way evcc can fully control the charging behaviour - if c.writeLoadControlLimitsVASVW(limits) { + if c.writeLoadControlLimitsVASVW(evEntity, limits) { return nil } // make sure the recommendations are inactive, otherwise the EV won't go to sleep - if recommendations, err := c.uc.OscEV.LoadControlLimits(evEntity); err == nil { - var writeNeeded bool - - for index, item := range recommendations { - if item.IsActive { - recommendations[index].IsActive = false - writeNeeded = true - } - } - - if writeNeeded { - _, _ = c.uc.OscEV.WriteLoadControlLimits(evEntity, recommendations, nil) - } + if err := c.disableLimits(evEntity, c.uc.OscEV); err != nil { + return err } - // Set overload protection limits - if _, err = c.uc.OpEV.WriteLoadControlLimits(evEntity, limits, nil); err == nil { - c.currentLimit = currents[0] - } + // set overload protection limits + _, err = c.uc.OpEV.WriteLoadControlLimits(evEntity, limits, nil) return err } // returns if the connected EV has an active VW PV mode // in this mode, the EV does not have an active charging demand -func (c *EEBus) hasActiveVASVW() bool { +func (c *EEBus) hasActiveVASVW(evEntity spineapi.EntityRemoteInterface) bool { // EVSE has to support VW VAS if !c.vasVW { return false } - evEntity := c.evEntity() - if evEntity == nil { - return false - } - // ISO15118-2 has to be used between EVSE and EV if comStandard, err := c.uc.EvCC.CommunicationStandard(evEntity); err != nil || comStandard != model.DeviceConfigurationKeyValueStringTypeISO151182ED2 { return false @@ -452,22 +369,19 @@ func (c *EEBus) hasActiveVASVW() bool { // the use case has to be reported as active // only then the EV has no active charging demand and will charge based on OSCEV recommendations // this is a workaround for EVSE changing isActive to false, even though they should - // not announce the usecase at all in that case - ucs := evEntity.Device().UseCases() - for _, item := range ucs { + // not announce the use case at all in that case + for _, uci := range evEntity.Device().UseCases() { // check if the referenced entity address is identical to the ev entity address // the address may not exist, as it only available since SPINE 1.3 - if item.Address != nil && + if uci.Address != nil && evEntity.Address() != nil && - slices.Compare(item.Address.Entity, evEntity.Address().Entity) != 0 { + slices.Compare(uci.Address.Entity, evEntity.Address().Entity) != 0 { continue } - for _, uc := range item.UseCaseSupport { - if uc.UseCaseName != nil && - *uc.UseCaseName == model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging && - uc.UseCaseAvailable != nil && - *uc.UseCaseAvailable == true { + for _, uc := range uci.UseCaseSupport { + if uc.UseCaseName != nil && *uc.UseCaseName == model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging && + uc.UseCaseAvailable != nil && *uc.UseCaseAvailable { return true } } @@ -481,13 +395,8 @@ func (c *EEBus) hasActiveVASVW() bool { // // this functionality allows to fully control charging without the EV actually having a // charging demand by itself -func (c *EEBus) writeLoadControlLimitsVASVW(limits []ucapi.LoadLimitsPhase) bool { - if !c.hasActiveVASVW() { - return false - } - - evEntity := c.evEntity() - if evEntity == nil { +func (c *EEBus) writeLoadControlLimitsVASVW(evEntity spineapi.EntityRemoteInterface, limits []ucapi.LoadLimitsPhase) bool { + if !c.hasActiveVASVW(evEntity) { return false } @@ -497,40 +406,57 @@ func (c *EEBus) writeLoadControlLimitsVASVW(limits []ucapi.LoadLimitsPhase) bool } // on OSCEV all limits have to be active except they are set to the default value - minLimit, _, _, err := c.uc.OscEV.CurrentLimits(evEntity) + minLimits, _, _, err := c.uc.OscEV.CurrentLimits(evEntity) if err != nil { + c.log.ERROR.Println("!! OscEV.CurrentLimits:", err) return false } for index, item := range limits { - limits[index].IsActive = item.Value >= minLimit[index] + limits[index].IsActive = item.Value >= minLimits[index] } - // send the write command + // set overload protection limits if _, err := c.uc.OscEV.WriteLoadControlLimits(evEntity, limits, nil); err != nil { + c.log.ERROR.Println("!! OscEV.WriteLoadControlLimits:", err) return false } - c.currentLimit = limits[0].Value - // make sure the obligations are inactive, otherwise the EV won't go to sleep - if obligations, err := c.uc.OpEV.LoadControlLimits(evEntity); err == nil { - writeNeeded := false - - for index, item := range obligations { - if item.IsActive { - obligations[index].IsActive = false - writeNeeded = true - } - } - - if writeNeeded { - _, _ = c.uc.OpEV.WriteLoadControlLimits(evEntity, obligations, nil) - } + if err := c.disableLimits(evEntity, c.uc.OpEV); err != nil { + c.log.ERROR.Println("!! OpEV.Load/WriteLoadControlLimits:", err) + return false } return true } +type eebusLimitController interface { + LoadControlLimits(spineapi.EntityRemoteInterface) ([]ucapi.LoadLimitsPhase, error) + WriteLoadControlLimits(spineapi.EntityRemoteInterface, []ucapi.LoadLimitsPhase, func(result model.ResultDataType)) (*model.MsgCounterType, error) +} + +// make sure the limits are inactive, otherwise the EV won't go to sleep +func (c *EEBus) disableLimits(evEntity spineapi.EntityRemoteInterface, uc eebusLimitController) error { + limits, err := uc.LoadControlLimits(evEntity) + if err != nil { + return err + } + + var writeNeeded bool + for index, item := range limits { + if item.IsActive { + limits[index].IsActive = false + writeNeeded = true + } + } + + if writeNeeded { + _, err = uc.WriteLoadControlLimits(evEntity, limits, nil) + } + + return err +} + // MaxCurrent implements the api.Charger interface func (c *EEBus) MaxCurrent(current int64) error { return c.MaxCurrentMillis(float64(current)) @@ -540,43 +466,29 @@ var _ api.ChargerEx = (*EEBus)(nil) // MaxCurrentMillis implements the api.ChargerEx interface func (c *EEBus) MaxCurrentMillis(current float64) error { - if !c.Connected() || c.evEntity() == nil { - return errors.New("can't set new current as ev is unplugged") + evEntity, ok := c.isEvConnected() + if !ok { + c.current = current + return nil } - if err := c.writeCurrentLimitData([]float64{current, current, current}); err != nil { - return err + err := c.writeCurrentLimitData(evEntity, current) + if err == nil { + c.current = current } - c.current = current - return nil } -var _ api.CurrentGetter = (*EEBus)(nil) - -// GetMaxCurrent implements the api.CurrentGetter interface -func (c *EEBus) GetMaxCurrent() (float64, error) { - c.mux.RLock() - defer c.mux.RUnlock() - - if c.currentLimit == -1 { - return 0, api.ErrNotAvailable - } - - return c.currentLimit, nil -} - // CurrentPower implements the api.Meter interface func (c *EEBus) currentPower() (float64, error) { - evEntity := c.evEntity() - if evEntity == nil { + evEntity, ok := c.isEvConnected() + if !ok { return 0, nil } - var powers []float64 - // does the EVSE provide power data? + var powers []float64 if c.uc.EvCem.IsScenarioAvailableAtEntity(evEntity, 2) { // is power data available for real? Elli Gen1 says it supports it, but doesn't provide any data if powerData, err := c.uc.EvCem.PowerPerPhase(evEntity); err == nil { @@ -599,18 +511,13 @@ func (c *EEBus) currentPower() (float64, error) { return 0, api.ErrNotAvailable } - var power float64 - for _, phasePower := range powers { - power += phasePower - } - - return power, nil + return lo.Sum(powers), nil } // ChargedEnergy implements the api.ChargeRater interface func (c *EEBus) chargedEnergy() (float64, error) { - evEntity := c.evEntity() - if evEntity == nil { + evEntity, ok := c.isEvConnected() + if !ok { return 0, nil } @@ -628,8 +535,8 @@ func (c *EEBus) chargedEnergy() (float64, error) { // Currents implements the api.PhaseCurrents interface func (c *EEBus) currents() (float64, float64, float64, error) { - evEntity := c.evEntity() - if evEntity == nil { + evEntity, ok := c.isEvConnected() + if !ok { return 0, 0, 0, nil } @@ -658,8 +565,8 @@ var _ api.Identifier = (*EEBus)(nil) // Identify implements the api.Identifier interface func (c *EEBus) Identify() (string, error) { - evEntity := c.evEntity() - if !c.Connected() || evEntity == nil { + evEntity, ok := c.isEvConnected() + if !ok { return "", nil } @@ -669,17 +576,6 @@ func (c *EEBus) Identify() (string, error) { return identification[0].Value, nil } - if comStandard, _ := c.uc.EvCC.CommunicationStandard(evEntity); comStandard == model.DeviceConfigurationKeyValueStringTypeIEC61851 { - return "", nil - } - - c.mux.RLock() - defer c.mux.RUnlock() - - if time.Since(c.connectedTime) < maxIdRequestTimespan { - return "", api.ErrMustRetry - } - return "", nil } @@ -687,7 +583,10 @@ var _ api.Battery = (*EEBus)(nil) // Soc implements the api.Vehicle interface func (c *EEBus) Soc() (float64, error) { - evEntity := c.evEntity() + evEntity, ok := c.isEvConnected() + if !ok { + return 0, nil + } if !c.uc.EvSoc.IsScenarioAvailableAtEntity(evEntity, 1) { return 0, api.ErrNotAvailable @@ -701,6 +600,36 @@ func (c *EEBus) Soc() (float64, error) { return soc, nil } +var _ api.CurrentLimiter = (*EEBus)(nil) + +func (c *EEBus) minMax() (minMax, error) { + var zero minMax + + evEntity, ok := c.isEvConnected() + if !ok { + return zero, nil + } + + minLimits, maxLimits, _, err := c.uc.OpEV.CurrentLimits(evEntity) + if err != nil { + if err == eebusapi.ErrDataNotAvailable { + err = api.ErrNotAvailable + } + return zero, err + } + + if len(minLimits) == 0 || len(maxLimits) == 0 { + return zero, api.ErrNotAvailable + } + + return minMax{minLimits[0], maxLimits[0]}, nil +} + +func (c *EEBus) GetMinMaxCurrent() (float64, float64, error) { + minMax, err := c.minMaxG() + return minMax.min, minMax.max, err +} + var _ loadpoint.Controller = (*EEBus)(nil) // LoadpointControl implements loadpoint.Controller diff --git a/charger/eebus_test.go b/charger/eebus_test.go index bd058d786..6971b8cef 100644 --- a/charger/eebus_test.go +++ b/charger/eebus_test.go @@ -10,23 +10,18 @@ import ( "github.com/evcc-io/evcc/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) 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 + charging bool + currents []float64 } tests := []struct { @@ -37,72 +32,52 @@ func TestEEBusIsCharging(t *testing.T) { { "3 phase IEC", []limitStruct{ - {1, 6, 16, 0}, - {2, 6, 16, 0}, - {3, 6, 16, 0}, + {6, 16, 0}, + {6, 16, 0}, + {6, 16, 0}, }, []testMeasurementStruct{ { false, - []measurementStruct{ - {1, 0}, - {2, 3}, - {3, 0}, - }, + []float64{0, 3, 0}, }, { true, - []measurementStruct{ - {1, 6}, - {2, 0}, - {3, 1}, - }, + []float64{6, 0, 1}, }, }, }, { "1 phase IEC", []limitStruct{ - {1, 6, 16, 0}, + {6, 16, 0}, }, []testMeasurementStruct{ { false, - []measurementStruct{ - {1, 2}, - }, + []float64{2}, }, { true, - []measurementStruct{ - {1, 6}, - }, + []float64{6}, }, }, }, { "3 phase ISO", []limitStruct{ - {1, 2.2, 16, 0.1}, - {2, 2.2, 16, 0.1}, - {3, 2.2, 16, 0.1}, + {2.2, 16, 0.1}, + {2.2, 16, 0.1}, + {2.2, 16, 0.1}, }, []testMeasurementStruct{ { false, - []measurementStruct{ - {1, 1}, - {2, 0}, - {3, 0}, - }, + []float64{1, 0, 0}, }, { true, - []measurementStruct{ - {1, 1.8}, - {2, 1}, - {3, 3}, - }, + []float64{1.8, 1, 3}, }, }, }, @@ -110,9 +85,7 @@ func TestEEBusIsCharging(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - limitsMin := make([]float64, 0) - limitsMax := make([]float64, 0) - limitsDefault := make([]float64, 0) + var limitsMin, limitsMax, limitsDefault []float64 for _, limit := range tc.limits { limitsMin = append(limitsMin, limit.min) @@ -120,38 +93,28 @@ func TestEEBusIsCharging(t *testing.T) { limitsDefault = append(limitsDefault, limit.pause) } - for index, m := range tc.measurements { + for _, m := range tc.measurements { ctrl := gomock.NewController(t) evcc := mocks.NewCemEVCCInterface(t) evcem := mocks.NewCemEVCEMInterface(t) opev := mocks.NewCemOPEVInterface(t) - uc := &eebus.UseCasesEVSE{ - EvCC: evcc, - EvCem: evcem, - OpEV: opev, - } evEntity := spinemocks.NewEntityRemoteInterface(t) eebus := &EEBus{ - uc: uc, + uc: &eebus.UseCasesEVSE{ + EvCC: evcc, + EvCem: evcem, + OpEV: opev, + }, ev: evEntity, } - currents := make([]float64, 0) - - for _, d := range m.data { - currents = append(currents, d.current) - } - - evcc.EXPECT().EVConnected(evEntity).Return(true) - evcem.EXPECT().CurrentPerPhase(evEntity).Return(currents, nil) + evcem.EXPECT().CurrentPerPhase(evEntity).Return(m.currents, nil) opev.EXPECT().CurrentLimits(evEntity).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) - } + require.Equal(t, m.charging, eebus.isCharging(evEntity)) + ctrl.Finish() } }) @@ -159,46 +122,48 @@ func TestEEBusIsCharging(t *testing.T) { } func TestEEBusCurrentPower(t *testing.T) { + evcc := mocks.NewCemEVCCInterface(t) evcem := mocks.NewCemEVCEMInterface(t) - uc := &eebus.UseCasesEVSE{ - EvCem: evcem, - } evEntity := spinemocks.NewEntityRemoteInterface(t) - logger := util.NewLogger("test") eebus := &EEBus{ - uc: uc, + uc: &eebus.UseCasesEVSE{ + EvCC: evcc, + EvCem: evcem, + }, ev: evEntity, - log: logger, + log: util.NewLogger("test"), } + evcc.EXPECT().EVConnected(evEntity).Return(true) evcem.EXPECT().IsScenarioAvailableAtEntity(evEntity, mock.Anything).Return(true) evcem.EXPECT().PowerPerPhase(evEntity).Return([]float64{600, 600, 600}, nil) power, err := eebus.currentPower() - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, 1800.0, power) } func TestEEBusCurrentPower_Elli(t *testing.T) { + evcc := mocks.NewCemEVCCInterface(t) evcem := mocks.NewCemEVCEMInterface(t) - uc := &eebus.UseCasesEVSE{ - EvCem: evcem, - } evEntity := spinemocks.NewEntityRemoteInterface(t) - logger := util.NewLogger("test") eebus := &EEBus{ - uc: uc, + uc: &eebus.UseCasesEVSE{ + EvCC: evcc, + EvCem: evcem, + }, ev: evEntity, - log: logger, + log: util.NewLogger("test"), } + evcc.EXPECT().EVConnected(evEntity).Return(true) evcem.EXPECT().IsScenarioAvailableAtEntity(evEntity, mock.Anything).Return(true) evcem.EXPECT().PowerPerPhase(evEntity).Return(nil, errors.New("error")) evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{5.8, 5.8, 5.8}, nil) power, err := eebus.currentPower() - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, 4002.0, power) } diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index 673920164..4a4c8c801 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -89,7 +89,7 @@ func NewEEBus(ski string, limits Limits, root api.Circuit) (*EEBus, error) { log: util.NewLogger("eebus"), root: root, uc: eebus.Instance.ControllableSystem(), - Connector: eebus.NewConnector(nil), + Connector: eebus.NewConnector(), heartbeat: provider.NewValue[struct{}](2 * time.Minute), // LPC-031 consumptionLimit: &ucapi.LoadLimit{ diff --git a/meter/eebus.go b/meter/eebus.go index db81000dd..1888b46f3 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -52,7 +52,7 @@ func NewEEBus(ski string, timeout time.Duration) (*EEBus, error) { c := &EEBus{ log: util.NewLogger("eebus"), uc: eebus.Instance.ControllableSystem(), - Connector: eebus.NewConnector(nil), + Connector: eebus.NewConnector(), power: provider.NewValue[float64](timeout), energy: provider.NewValue[float64](timeout), voltages: provider.NewValue[[]float64](timeout), diff --git a/server/eebus/connector.go b/server/eebus/connector.go index 7ea7ce0a6..e9f7dfe6a 100644 --- a/server/eebus/connector.go +++ b/server/eebus/connector.go @@ -8,19 +8,12 @@ import ( ) type Connector struct { - cb func(connected bool) - - mu sync.RWMutex - once sync.Once - connected bool - connectC chan struct{} + once sync.Once + connectC chan struct{} } -func NewConnector(cb func(connected bool)) *Connector { - return &Connector{ - cb: cb, - connectC: make(chan struct{}), - } +func NewConnector() *Connector { + return &Connector{connectC: make(chan struct{})} } func (c *Connector) Wait(timeout time.Duration) error { @@ -33,21 +26,7 @@ func (c *Connector) Wait(timeout time.Duration) error { } func (c *Connector) Connect(connected bool) { - c.mu.Lock() - c.connected = connected - c.mu.Unlock() - if connected { c.once.Do(func() { close(c.connectC) }) } - - if c.cb != nil { - c.cb(connected) - } -} - -func (c *Connector) Connected() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.connected }