EEBus: simplify charger logic (#15410)

This commit is contained in:
andig 2024-08-18 12:24:30 +02:00 • committed by GitHub
parent b57058e7c1
commit aa86f8458e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 216 additions and 343 deletions

View file

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

View file

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

View file

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

View file

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

View file

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