evcc-io/charger/eebus.go
Martin Weinelt daee304a49
chore: migrate from unmaintained mockgen (#11547)
The golang mock package has been archived and is now maintained by uber.

> Update, June 2023: This repo and tool are no longer maintained. Please
> see go.uber.org/mock for a maintained fork instead.

[1] https://github.com/golang/mock
[2] https://github.com/uber-go/mock
2024-01-08 11:13:27 +01:00

512 lines
13 KiB
Go

package charger
import (
"errors"
"fmt"
"os"
"sync"
"time"
"github.com/enbility/cemd/emobility"
"github.com/enbility/eebus-go/features"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/eebus"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/provider"
"github.com/evcc-io/evcc/util"
)
//go:generate mockgen -package charger -destination eebus_test_mock.go github.com/enbility/cemd/emobility EmobilityI
const (
maxIdRequestTimespan = time.Second * 120
idleFactor = 0.6
voltage float64 = 230
)
type minMax struct {
min, max float64
}
type EEBus struct {
ski string
emobility emobility.EmobilityI
log *util.Logger
lp loadpoint.API
minMaxG func() (minMax, error)
communicationStandard emobility.EVCommunicationStandardType
expectedEnableUnpluggedState bool
current float64
// connection tracking for api.CurrentGetter
evConnected bool
currentLimit float64
lastIsChargingCheck time.Time
lastIsChargingResult bool
connected bool
connectedC chan bool
connectedTime time.Time
mux sync.Mutex
}
func init() {
registry.Add("eebus", NewEEBusFromConfig)
}
// NewEEBusFromConfig creates an EEBus charger from generic config
func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
Ski string
Ip string
Meter bool
ChargedEnergy bool
}{
ChargedEnergy: true,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewEEBus(cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy)
}
//go:generate go run ../cmd/tools/decorate.go -f decorateEEBus -b *EEBus -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.ChargeRater,ChargedEnergy,func() (float64, error)"
// NewEEBus creates EEBus charger
func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy bool) (api.Charger, error) {
log := util.NewLogger("eebus")
if eebus.Instance == nil {
return nil, errors.New("eebus not configured")
}
c := &EEBus{
ski: ski,
log: log,
connectedC: make(chan bool, 1),
communicationStandard: emobility.EVCommunicationStandardTypeUnknown,
current: 6,
}
c.emobility = eebus.Instance.RegisterEVSE(ski, ip, c.onConnect, c.onDisconnect, nil)
c.minMaxG = provider.Cached(c.minMax, time.Second)
if err := c.waitForConnection(); err != nil {
return c, err
}
if hasMeter {
var energyG func() (float64, error)
if hasChargedEnergy {
energyG = c.chargedEnergy
}
return decorateEEBus(c, c.currentPower, c.currents, energyG), nil
}
return c, nil
}
// waitForConnection wait for initial connection and returns an error on failure
func (c *EEBus) waitForConnection() error {
timeout := time.After(90 * time.Second)
for {
select {
case <-timeout:
return os.ErrDeadlineExceeded
case connected := <-c.connectedC:
if connected {
return nil
}
}
}
}
func (c *EEBus) onConnect(ski string) {
c.log.TRACE.Println("connect ski:", ski)
c.expectedEnableUnpluggedState = false
c.setDefaultValues()
c.setConnected(true)
}
func (c *EEBus) onDisconnect(ski string) {
c.log.TRACE.Println("disconnect ski:", ski)
c.expectedEnableUnpluggedState = false
c.setConnected(false)
c.setDefaultValues()
}
func (c *EEBus) setDefaultValues() {
c.communicationStandard = emobility.EVCommunicationStandardTypeUnknown
c.lastIsChargingCheck = time.Now().Add(-time.Hour * 1)
c.lastIsChargingResult = false
}
func (c *EEBus) setConnected(connected bool) {
c.mux.Lock()
defer c.mux.Unlock()
if connected && !c.connected {
c.connectedTime = time.Now()
}
select {
case c.connectedC <- connected:
default:
}
c.connected = connected
}
func (c *EEBus) isConnected() bool {
c.mux.Lock()
defer c.mux.Unlock()
return c.connected
}
var _ api.CurrentLimiter = (*EEBus)(nil)
func (c *EEBus) minMax() (minMax, error) {
minLimits, maxLimits, _, err := c.emobility.EVCurrentLimits()
if err != nil {
if err == features.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
}
// 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 { // d *communication.EVSEClientDataType
// 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
if c.lp != nil && c.lp.HasChargeMeter() {
// we only check ever 10 seconds, maybe we can use the config interval duration
if time.Since(c.lastIsChargingCheck) >= 10*time.Second {
c.lastIsChargingCheck = time.Now()
c.lastIsChargingResult = false
// compare charge power for all phases to 0.6 * min. charge power of a single phase
if c.lp.GetChargePower() > c.lp.EffectiveMinPower()*idleFactor {
c.lastIsChargingResult = true
return true
}
} else if c.lastIsChargingResult {
return true
}
}
// The above doesn't (yet) work for built in meters, so check the EEBUS measurements also
currents, err := c.emobility.EVCurrentsPerPhase()
if err != nil {
return false
}
limitsMin, _, _, err := c.emobility.EVCurrentLimits()
if err != nil || limitsMin == nil || len(limitsMin) == 0 {
return false
}
var phasesCurrent float64
for _, phaseCurrent := range currents {
phasesCurrent += phaseCurrent
}
// 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
}
// Status implements the api.Charger interface
func (c *EEBus) Status() (api.ChargeStatus, error) {
if !c.isConnected() {
return api.StatusNone, api.ErrTimeout
}
if !c.emobility.EVConnected() {
c.expectedEnableUnpluggedState = false
c.evConnected = false
return api.StatusA, nil
}
if !c.evConnected {
c.evConnected = true
c.currentLimit = -1
}
currentState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return api.StatusNone, err
}
switch currentState {
case emobility.EVChargeStateTypeUnknown, emobility.EVChargeStateTypeUnplugged: // Unplugged
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() {
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)
}
}
// Enabled implements the api.Charger interface
// 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
state, err := c.Status()
if err != nil || state == api.StatusA {
return c.expectedEnableUnpluggedState, nil
}
// if the EV is charging
if state == api.StatusC {
return true, 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 {
// 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
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.emobility.EVCommunicationStandard()
if err != nil || comStandard == emobility.EVCommunicationStandardTypeUnknown {
return api.ErrMustRetry
}
}
var current float64
if enable {
current = c.current
}
return c.writeCurrentLimitData([]float64{current, current, current})
}
// send current charging power limits to the EV
func (c *EEBus) writeCurrentLimitData(currents []float64) error {
comStandard, err := c.emobility.EVCommunicationStandard()
if err != nil {
return err
}
// Only send currents smaller than 6A if the communication standard is known.
// Otherwise this could cause ISO15118 capable OBCs to stick with IEC61851 when plugging
// the charge cable in. Or even worse show an error and the cable then needs to be unplugged,
// wait for the car to go into sleep and plug it back in.
// So if there are currents smaller than 6A with unknown communication standard change them to 6A.
// Keep in mind that this will still confuse evcc as it thinks charging is stopped, but it hasn't yet.
if comStandard == emobility.EVCommunicationStandardTypeUnknown {
minLimits, _, _, err := c.emobility.EVCurrentLimits()
if err == nil {
for index, current := range currents {
if index < len(minLimits) && current < minLimits[index] {
currents[index] = minLimits[index]
}
}
}
}
// Set overload protection limits and self consumption limits to identical values,
// so if the EV supports self consumption it will be used automatically.
if err = c.emobility.EVWriteLoadControlLimits(currents, currents); err == nil {
c.currentLimit = currents[0]
}
return err
}
// MaxCurrent implements the api.Charger interface
func (c *EEBus) MaxCurrent(current int64) error {
return c.MaxCurrentMillis(float64(current))
}
var _ api.ChargerEx = (*EEBus)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (c *EEBus) MaxCurrentMillis(current float64) error {
if !c.connected || !c.emobility.EVConnected() {
return errors.New("can't set new current as ev is unplugged")
}
if err := c.writeCurrentLimitData([]float64{current, current, current}); err != nil {
return err
}
c.current = current
return nil
}
var _ api.CurrentGetter = (*Easee)(nil)
// GetMaxCurrent implements the api.CurrentGetter interface
func (c *EEBus) GetMaxCurrent() (float64, error) {
return c.currentLimit, nil
}
// CurrentPower implements the api.Meter interface
func (c *EEBus) currentPower() (float64, error) {
if !c.emobility.EVConnected() {
return 0, nil
}
connectedPhases, err := c.emobility.EVConnectedPhases()
if err != nil {
return 0, err
}
powers, err := c.emobility.EVPowerPerPhase()
if err != nil {
return 0, err
}
var power float64
for index, phasePower := range powers {
if index >= int(connectedPhases) {
break
}
power += phasePower
}
return power, nil
}
// ChargedEnergy implements the api.ChargeRater interface
func (c *EEBus) chargedEnergy() (float64, error) {
if !c.emobility.EVConnected() {
return 0, nil
}
energy, err := c.emobility.EVChargedEnergy()
if err != nil {
return 0, err
}
return energy / 1e3, nil
}
// Currents implements the api.PhaseCurrents interface
func (c *EEBus) currents() (float64, float64, float64, error) {
if !c.emobility.EVConnected() {
return 0, 0, 0, nil
}
res, err := c.emobility.EVCurrentsPerPhase()
if err != nil {
if err == features.ErrDataNotAvailable {
err = api.ErrNotAvailable
}
return 0, 0, 0, err
}
// fill phases
for len(res) < 3 {
res = append(res, 0)
}
return res[0], res[1], res[2], nil
}
var _ api.Identifier = (*EEBus)(nil)
// Identify implements the api.Identifier interface
func (c *EEBus) Identify() (string, error) {
if !c.isConnected() || !c.emobility.EVConnected() {
return "", nil
}
if !c.emobility.EVConnected() {
return "", nil
}
if identification, _ := c.emobility.EVIdentification(); identification != "" {
return identification, nil
}
if comStandard, _ := c.emobility.EVCommunicationStandard(); comStandard == emobility.EVCommunicationStandardTypeIEC61851 {
return "", nil
}
if time.Since(c.connectedTime) < maxIdRequestTimespan {
return "", api.ErrMustRetry
}
return "", nil
}
var _ api.Battery = (*EEBus)(nil)
// Soc implements the api.Vehicle interface
func (c *EEBus) Soc() (float64, error) {
if socSupported, err := c.emobility.EVSoCSupported(); err != nil || !socSupported {
return 0, api.ErrNotAvailable
}
soc, err := c.emobility.EVSoC()
if err != nil {
return 0, api.ErrNotAvailable
}
return soc, nil
}
var _ loadpoint.Controller = (*EEBus)(nil)
// LoadpointControl implements loadpoint.Controller
func (c *EEBus) LoadpointControl(lp loadpoint.API) {
c.lp = lp
}