EEBus: simplify charger logic (#15410)
This commit is contained in:
parent
b57058e7c1
commit
aa86f8458e
5 changed files with 216 additions and 343 deletions
407
charger/eebus.go
407
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue