EEbus: upgrade libraries (#13998)

This updates the eebus charger implementation to use the latest version of cemd, eebus, spine and ship.
Only use OSCEV if the a wallbox with ISO15118-2 VW VAS is connected, and the EV is using this. This also enables full charging control and putting the EV to sleep. All other current implementations of the usecase are basically wrong and are no different than using OPEV.
This commit is contained in:
Andreas Linde 2024-06-30 14:23:40 +02:00 • committed by GitHub
parent 737ca58db1
commit b2f68162bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 517 additions and 577 deletions

View file

@ -4,11 +4,15 @@ import (
"errors"
"fmt"
"os"
"slices"
"sync"
"time"
"github.com/enbility/cemd/emobility"
"github.com/enbility/eebus-go/features"
eebusapi "github.com/enbility/eebus-go/api"
ucapi "github.com/enbility/eebus-go/usecases/api"
"github.com/enbility/eebus-go/usecases/cem/evcc"
spineapi "github.com/enbility/spine-go/api"
"github.com/enbility/spine-go/model"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/eebus"
"github.com/evcc-io/evcc/core/loadpoint"
@ -16,8 +20,6 @@ import (
"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
@ -29,20 +31,21 @@ type minMax struct {
}
type EEBus struct {
ski string
emobility emobility.EmobilityI
ski string
uc *eebus.UseCasesEVSE
ev spineapi.EntityRemoteInterface
log *util.Logger
lp loadpoint.API
minMaxG func() (minMax, error)
communicationStandard emobility.EVCommunicationStandardType
communicationStandard model.DeviceConfigurationKeyValueStringType
vasVW bool // wether the EVSE supports VW VAS with ISO15118-2
expectedEnableUnpluggedState bool
current float64
// connection tracking for api.CurrentGetter
evConnected bool
currentLimit float64
lastIsChargingCheck time.Time
@ -52,7 +55,8 @@ type EEBus struct {
connectedC chan bool
connectedTime time.Time
mux sync.Mutex
muxEntity sync.Mutex
mux sync.Mutex
}
func init() {
@ -63,9 +67,10 @@ func init() {
func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
Ski string
Ip string
Ip_ string `mapstructure:"ip"` // deprecated
Meter bool
ChargedEnergy bool
VasVW bool
}{
ChargedEnergy: true,
}
@ -74,13 +79,13 @@ func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, err
}
return NewEEBus(cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy)
return NewEEBus(cc.Ski, cc.Meter, cc.ChargedEnergy, cc.VasVW)
}
//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) {
func NewEEBus(ski string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, error) {
log := util.NewLogger("eebus")
if eebus.Instance == nil {
@ -88,14 +93,14 @@ func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy bool) (api.Charger, err
}
c := &EEBus{
ski: ski,
log: log,
connectedC: make(chan bool, 1),
communicationStandard: emobility.EVCommunicationStandardTypeUnknown,
current: 6,
ski: ski,
log: log,
connectedC: make(chan bool, 1),
current: 6,
vasVW: vasVW,
}
c.emobility = eebus.Instance.RegisterEVSE(ski, ip, c.onConnect, c.onDisconnect, nil)
c.uc = eebus.Instance.RegisterEVSE(ski, c)
c.minMaxG = provider.Cached(c.minMax, time.Second)
@ -129,28 +134,60 @@ func (c *EEBus) waitForConnection() error {
}
}
func (c *EEBus) onConnect(ski string) {
c.log.TRACE.Println("connect ski:", ski)
func (c *EEBus) setEvEntity(entity spineapi.EntityRemoteInterface) {
c.muxEntity.Lock()
defer c.muxEntity.Unlock()
c.ev = entity
}
func (c *EEBus) evEntity() spineapi.EntityRemoteInterface {
c.muxEntity.Lock()
defer c.muxEntity.Unlock()
return c.ev
}
// EEBUSDeviceInterface
func (c *EEBus) DeviceConnect() {
c.log.TRACE.Println("connect ski:", c.ski)
c.expectedEnableUnpluggedState = false
c.setDefaultValues()
c.setConnected(true)
}
func (c *EEBus) onDisconnect(ski string) {
c.log.TRACE.Println("disconnect ski:", ski)
func (c *EEBus) DeviceDisconnect() {
c.log.TRACE.Println("disconnect ski:", c.ski)
c.expectedEnableUnpluggedState = false
c.setConnected(false)
c.setDefaultValues()
}
// UseCase specific events
func (c *EEBus) UseCaseEventCB(device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) {
switch event {
// EV
case evcc.EvConnected:
c.log.TRACE.Println("EV Connected")
c.setEvEntity(entity)
c.currentLimit = -1
case evcc.EvDisconnected:
c.log.TRACE.Println("EV Disconnected")
c.setEvEntity(nil)
c.currentLimit = -1
}
}
func (c *EEBus) setDefaultValues() {
c.communicationStandard = emobility.EVCommunicationStandardTypeUnknown
c.communicationStandard = evcc.EVCCCommunicationStandardUnknown
c.lastIsChargingCheck = time.Now().Add(-time.Hour * 1)
c.lastIsChargingResult = false
}
// set wether the EVSE is connected
func (c *EEBus) setConnected(connected bool) {
c.mux.Lock()
defer c.mux.Unlock()
@ -177,9 +214,14 @@ func (c *EEBus) isConnected() bool {
var _ api.CurrentLimiter = (*EEBus)(nil)
func (c *EEBus) minMax() (minMax, error) {
minLimits, maxLimits, _, err := c.emobility.EVCurrentLimits()
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 == features.ErrDataNotAvailable {
if err == eebusapi.ErrDataNotAvailable {
err = api.ErrNotAvailable
}
return minMax{}, err
@ -198,7 +240,12 @@ func (c *EEBus) GetMinMaxCurrent() (float64, float64, error) {
}
// 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
func (c *EEBus) isCharging() bool {
evEntity := c.evEntity()
if !c.uc.EvCC.EVConnected(evEntity) {
return false
}
// 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
@ -218,11 +265,11 @@ func (c *EEBus) isCharging() bool { // d *communication.EVSEClientDataType
}
// The above doesn't (yet) work for built in meters, so check the EEBUS measurements also
currents, err := c.emobility.EVCurrentsPerPhase()
currents, err := c.uc.EvCem.CurrentPerPhase(evEntity)
if err != nil {
return false
}
limitsMin, _, _, err := c.emobility.EVCurrentLimits()
limitsMin, _, _, err := c.uc.OpEV.CurrentLimits(evEntity)
if err != nil || limitsMin == nil || len(limitsMin) == 0 {
return false
}
@ -241,38 +288,33 @@ func (c *EEBus) isCharging() bool { // d *communication.EVSEClientDataType
// Status implements the api.Charger interface
func (c *EEBus) Status() (api.ChargeStatus, error) {
evEntity := c.evEntity()
if !c.isConnected() {
return api.StatusNone, api.ErrTimeout
}
if !c.emobility.EVConnected() {
if !c.uc.EvCC.EVConnected(evEntity) {
c.expectedEnableUnpluggedState = false
c.evConnected = false
return api.StatusA, nil
}
if !c.evConnected {
c.evConnected = true
c.currentLimit = -1
}
currentState, err := c.emobility.EVCurrentChargeState()
currentState, err := c.uc.EvCC.ChargeState(evEntity)
if err != nil {
return api.StatusNone, err
}
switch currentState {
case emobility.EVChargeStateTypeUnknown, emobility.EVChargeStateTypeUnplugged: // Unplugged
case ucapi.EVChargeStateTypeUnknown, ucapi.EVChargeStateTypeUnplugged: // Unplugged
c.expectedEnableUnpluggedState = false
return api.StatusA, nil
case emobility.EVChargeStateTypeFinished, emobility.EVChargeStateTypePaused: // Finished, Paused
case ucapi.EVChargeStateTypeFinished, ucapi.EVChargeStateTypePaused: // Finished, Paused
return api.StatusB, nil
case emobility.EVChargeStateTypeActive: // Active
case ucapi.EVChargeStateTypeActive: // Active
if c.isCharging() {
return api.StatusC, nil
}
return api.StatusB, nil
case emobility.EVChargeStateTypeError: // Error
case ucapi.EVChargeStateTypeError: // Error
return api.StatusF, nil
default:
return api.StatusNone, fmt.Errorf("%s properties unknown result: %s", c.ski, currentState)
@ -283,8 +325,9 @@ func (c *EEBus) Status() (api.ChargeStatus, 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 {
if err != nil || state == api.StatusA || evEntity == nil {
return c.expectedEnableUnpluggedState, nil
}
@ -293,9 +336,27 @@ func (c *EEBus) Enabled() (bool, error) {
return true, nil
}
limits, err := c.emobility.EVLoadControlObligationLimits()
// if the VW VAS PV mode is active, use PV limits
if c.hasActiveVASVW() {
limits, err := c.uc.OscEV.LoadControlLimits(evEntity)
if err != nil {
// there are no limits available, e.g. because the data was not received yet
return true, nil
}
for _, limit := range limits {
// check if there is an active limit set
if limit.IsActive && limit.Value >= 1 {
return true, nil
}
}
return false, nil
}
limits, err := c.uc.OpEV.LoadControlLimits(evEntity)
if err != nil {
// there are no overload protection limits available, e.g. because the data was not received yet
// there are limits available, e.g. because the data was not received yet
return true, nil
}
@ -303,7 +364,9 @@ func (c *EEBus) Enabled() (bool, error) {
// 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 {
// if the limit is not active, then the maximum possible current is permitted
if (limit.IsActive && limit.Value >= 1) ||
!limit.IsActive {
return true, nil
}
}
@ -322,8 +385,8 @@ func (c *EEBus) Enable(enable bool) error {
// 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 {
comStandard, err := c.uc.EvCC.CommunicationStandard(c.evEntity())
if err != nil || comStandard == evcc.EVCCCommunicationStandardUnknown {
return api.ErrMustRetry
}
}
@ -339,37 +402,172 @@ func (c *EEBus) Enable(enable bool) error {
// 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
evEntity := c.evEntity()
if !c.uc.EvCC.EVConnected(evEntity) {
return errors.New("no ev connected")
}
// 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]
}
_, maxLimits, _, err := c.uc.OpEV.CurrentLimits(evEntity)
if err != nil {
return errors.New("no limits available")
}
// setup the limit data structure
limits := []ucapi.LoadLimitsPhase{}
for phase, current := range currents {
if phase >= len(maxLimits) || phase >= len(ucapi.PhaseNameMapping) {
continue
}
limit := ucapi.LoadLimitsPhase{
Phase: ucapi.PhaseNameMapping[phase],
IsActive: true,
Value: current,
}
// if the limit equals to the max allowed, then the obligation limit is actually inactive
if current >= maxLimits[phase] {
limit.IsActive = false
}
limits = append(limits, limit)
}
// if VAS VW is available, limits are completely covered by it
// this way evcc can fully control the charging behaviour
if c.writeLoadControlLimitsVASVW(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 _, item := range recommendations {
if item.IsActive {
item.IsActive = false
writeNeeded = true
}
}
if writeNeeded {
_, _ = c.uc.OscEV.WriteLoadControlLimits(evEntity, recommendations, nil)
}
}
// 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 {
// Set overload protection limits
if _, err = c.uc.OpEV.WriteLoadControlLimits(evEntity, limits, nil); err == nil {
c.currentLimit = currents[0]
}
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 {
// 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
}
// SoC has to be available, otherwise it is plain ISO15118-2
if _, err := c.Soc(); err != nil {
return false
}
// Optimization of self consumption use case support has to be available
if !c.uc.EvSoc.IsScenarioAvailableAtEntity(evEntity, 1) {
return false
}
// 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 {
// 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 &&
evEntity.Address() != nil &&
slices.Compare(item.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 {
return true
}
}
}
return false
}
// provides support for the special VW VAS ISO15118-2 charging behaviour if supported
// will return false if it isn't supported or successful
//
// 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 {
return false
}
// on OSCEV all limits have to be active except they are set to the default value
minLimit, _, _, err := c.uc.OscEV.CurrentLimits(evEntity)
if err != nil {
return false
}
for index, item := range limits {
limits[index].IsActive = item.Value >= minLimit[index]
}
// send the write command
if _, err := c.uc.OscEV.WriteLoadControlLimits(evEntity, limits, nil); err != nil {
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)
}
}
return true
}
// MaxCurrent implements the api.Charger interface
func (c *EEBus) MaxCurrent(current int64) error {
return c.MaxCurrentMillis(float64(current))
@ -379,7 +577,7 @@ var _ api.ChargerEx = (*EEBus)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (c *EEBus) MaxCurrentMillis(current float64) error {
if !c.connected || !c.emobility.EVConnected() {
if !c.connected || c.evEntity() == nil {
return errors.New("can't set new current as ev is unplugged")
}
@ -392,25 +590,30 @@ func (c *EEBus) MaxCurrentMillis(current float64) error {
return nil
}
var _ api.CurrentGetter = (*Easee)(nil)
var _ api.CurrentGetter = (*EEBus)(nil)
// GetMaxCurrent implements the api.CurrentGetter interface
func (c *EEBus) GetMaxCurrent() (float64, error) {
if c.currentLimit == -1 {
return 0, api.ErrNotAvailable
}
return c.currentLimit, nil
}
// CurrentPower implements the api.Meter interface
func (c *EEBus) currentPower() (float64, error) {
if !c.emobility.EVConnected() {
evEntity := c.evEntity()
if evEntity == nil {
return 0, nil
}
connectedPhases, err := c.emobility.EVConnectedPhases()
connectedPhases, err := c.uc.EvCem.PhasesConnected(evEntity)
if err != nil {
return 0, err
}
powers, err := c.emobility.EVPowerPerPhase()
powers, err := c.uc.EvCem.PowerPerPhase(evEntity)
if err != nil {
return 0, err
}
@ -428,11 +631,12 @@ func (c *EEBus) currentPower() (float64, error) {
// ChargedEnergy implements the api.ChargeRater interface
func (c *EEBus) chargedEnergy() (float64, error) {
if !c.emobility.EVConnected() {
evEntity := c.evEntity()
if evEntity == nil {
return 0, nil
}
energy, err := c.emobility.EVChargedEnergy()
energy, err := c.uc.EvCem.EnergyCharged(evEntity)
if err != nil {
return 0, err
}
@ -442,13 +646,14 @@ func (c *EEBus) chargedEnergy() (float64, error) {
// Currents implements the api.PhaseCurrents interface
func (c *EEBus) currents() (float64, float64, float64, error) {
if !c.emobility.EVConnected() {
evEntity := c.evEntity()
if evEntity == nil {
return 0, 0, 0, nil
}
res, err := c.emobility.EVCurrentsPerPhase()
res, err := c.uc.EvCem.CurrentPerPhase(evEntity)
if err != nil {
if err == features.ErrDataNotAvailable {
if err == eebusapi.ErrDataNotAvailable {
err = api.ErrNotAvailable
}
return 0, 0, 0, err
@ -466,18 +671,18 @@ var _ api.Identifier = (*EEBus)(nil)
// Identify implements the api.Identifier interface
func (c *EEBus) Identify() (string, error) {
if !c.isConnected() || !c.emobility.EVConnected() {
evEntity := c.evEntity()
if !c.isConnected() || evEntity == nil {
return "", nil
}
if !c.emobility.EVConnected() {
return "", nil
}
if identification, _ := c.emobility.EVIdentification(); identification != "" {
return identification, nil
if identification, err := c.uc.EvCC.Identifications(evEntity); err == nil && len(identification) > 0 {
// return the first identification for now
// later this could be multiple, e.g. MAC Address and PCID
return identification[0].Value, nil
}
if comStandard, _ := c.emobility.EVCommunicationStandard(); comStandard == emobility.EVCommunicationStandardTypeIEC61851 {
if comStandard, _ := c.uc.EvCC.CommunicationStandard(evEntity); comStandard == model.DeviceConfigurationKeyValueStringTypeIEC61851 {
return "", nil
}
@ -492,11 +697,13 @@ 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 {
evEntity := c.evEntity()
if !c.uc.EvSoc.IsScenarioAvailableAtEntity(evEntity, 1) {
return 0, api.ErrNotAvailable
}
soc, err := c.emobility.EVSoC()
soc, err := c.uc.EvSoc.StateOfCharge(evEntity)
if err != nil {
return 0, api.ErrNotAvailable
}

View file

@ -0,0 +1,77 @@
package eebus
import (
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/enbility/ship-go/cert"
)
// CreateCertificate returns a newly created EEBUS compatible certificate
func CreateCertificate() (tls.Certificate, error) {
return cert.CreateCertificate("", EEBUSBrandName, "DE", EEBUSDeviceCode)
}
// pemBlockForKey marshals private key into pem block
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil, fmt.Errorf("unable to marshal ECDSA private key: %w", err)
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
default:
return nil, errors.New("unknown private key type")
}
}
// GetX509KeyPair saves returns the cert and key string values
func GetX509KeyPair(cert tls.Certificate) (string, string, error) {
var certValue, keyValue string
out := new(bytes.Buffer)
err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]})
if err == nil {
certValue = out.String()
}
if len(certValue) > 0 {
var pb *pem.Block
if pb, err = pemBlockForKey(cert.PrivateKey); err == nil {
out.Reset()
err = pem.Encode(out, pb)
}
}
if err == nil {
keyValue = out.String()
}
return certValue, keyValue, err
}
// SkiFromX509 extracts SKI from certificate
func skiFromX509(leaf *x509.Certificate) (string, error) {
if len(leaf.SubjectKeyId) == 0 {
return "", errors.New("missing SubjectKeyId")
}
return fmt.Sprintf("%0x", leaf.SubjectKeyId), nil
}
// SkiFromCert extracts SKI from certificate
func SkiFromCert(cert tls.Certificate) (string, error) {
leaf, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return "", errors.New("failed parsing certificate: " + err.Error())
}
return skiFromX509(leaf)
}

View file

@ -1,24 +1,27 @@
package eebus
import (
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
"dario.cat/mergo"
"github.com/enbility/cemd/cem"
"github.com/enbility/cemd/emobility"
"github.com/enbility/eebus-go/service"
"github.com/enbility/eebus-go/spine/model"
eebusapi "github.com/enbility/eebus-go/api"
service "github.com/enbility/eebus-go/service"
ucapi "github.com/enbility/eebus-go/usecases/api"
"github.com/enbility/eebus-go/usecases/cem/evcc"
"github.com/enbility/eebus-go/usecases/cem/evcem"
"github.com/enbility/eebus-go/usecases/cem/evsecc"
"github.com/enbility/eebus-go/usecases/cem/evsoc"
"github.com/enbility/eebus-go/usecases/cem/opev"
"github.com/enbility/eebus-go/usecases/cem/oscev"
shipapi "github.com/enbility/ship-go/api"
shiputil "github.com/enbility/ship-go/util"
spineapi "github.com/enbility/spine-go/api"
"github.com/enbility/spine-go/model"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/machine"
)
@ -43,20 +46,33 @@ func (c Config) Configured() bool {
return len(c.Certificate.Public) > 0 && len(c.Certificate.Private) > 0
}
type EEBusClientCBs struct {
onConnect func(string) // , ship.Conn) error
onDisconnect func(string)
type EEBUSDeviceInterface interface {
DeviceConnect()
DeviceDisconnect()
UseCaseEventCB(device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType)
}
// EVSE UseCases
type UseCasesEVSE struct {
EvseCC ucapi.CemEVSECCInterface
EvCC ucapi.CemEVCCInterface
EvCem ucapi.CemEVCEMInterface
EvSoc ucapi.CemEVSOCInterface
OpEV ucapi.CemOPEVInterface
OscEV ucapi.CemOSCEVInterface
}
type EEBus struct {
Cem *cem.CemImpl
service eebusapi.ServiceInterface
evseUC *UseCasesEVSE
mux sync.Mutex
log *util.Logger
SKI string
clients map[string]EEBusClientCBs
clients map[string]EEBUSDeviceInterface
}
var Instance *EEBus
@ -98,19 +114,20 @@ func NewServer(other Config) (*EEBus, error) {
}
// TODO: get the voltage from the site
configuration, err := service.NewConfiguration(
configuration, err := eebusapi.NewConfiguration(
EEBUSBrandName, EEBUSBrandName, EEBUSModel, serial,
model.DeviceTypeTypeEnergyManagementSystem, port, certificate, 230,
model.DeviceTypeTypeEnergyManagementSystem,
[]model.EntityTypeType{model.EntityTypeTypeCEM},
port, certificate, 230, time.Second*4,
)
if err != nil {
return nil, err
}
// for backward compatibility
configuration.SetAlternateMdnsServiceName("EVCC_HEMS_01")
configuration.SetAlternateMdnsServiceName(EEBUSDeviceCode)
configuration.SetAlternateIdentifier(serial)
configuration.SetInterfaces(cc.Interfaces)
configuration.SetRegisterAutoAccept(true)
ski, err := SkiFromCert(certificate)
if err != nil {
@ -119,81 +136,123 @@ func NewServer(other Config) (*EEBus, error) {
c := &EEBus{
log: log,
clients: make(map[string]EEBusClientCBs),
clients: make(map[string]EEBUSDeviceInterface),
SKI: ski,
}
c.Cem = cem.NewCEM(configuration, c, c)
if err := c.Cem.Setup(); err != nil {
c.service = service.NewService(configuration, c)
c.service.SetLogging(c)
if err := c.service.Setup(); err != nil {
return nil, err
}
c.Cem.EnableEmobility(emobility.EmobilityConfiguration{
CoordinatedChargingEnabled: false,
})
localEntity := c.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM)
c.evseUC = &UseCasesEVSE{
EvseCC: evsecc.NewEVSECC(localEntity, c.evseUsecaseCB),
EvCC: evcc.NewEVCC(c.service, localEntity, c.evseUsecaseCB),
EvCem: evcem.NewEVCEM(c.service, localEntity, c.evseUsecaseCB),
OpEV: opev.NewOPEV(localEntity, c.evseUsecaseCB),
OscEV: oscev.NewOSCEV(localEntity, c.evseUsecaseCB),
EvSoc: evsoc.NewEVSOC(localEntity, c.evseUsecaseCB),
}
// register use cases
for _, uc := range []eebusapi.UseCaseInterface{
c.evseUC.EvseCC, c.evseUC.EvCC,
c.evseUC.EvCem, c.evseUC.OpEV,
c.evseUC.OscEV, c.evseUC.EvSoc,
} {
c.service.AddUseCase(uc)
}
return c, nil
}
func (c *EEBus) RegisterEVSE(ski, ip string, connectHandler func(string), disconnectHandler func(string), dataProvider emobility.EmobilityDataProvider) *emobility.EMobilityImpl {
ski = strings.ReplaceAll(ski, "-", "")
ski = strings.ReplaceAll(ski, " ", "")
ski = strings.ToLower(ski)
func (c *EEBus) RegisterEVSE(ski string, device EEBUSDeviceInterface) *UseCasesEVSE {
ski = shiputil.NormalizeSKI(ski)
c.log.TRACE.Printf("registering ski: %s", ski)
if ski == c.SKI {
c.log.FATAL.Fatal("The charger SKI can not be identical to the SKI of evcc!")
}
serviceDetails := service.NewServiceDetails(ski)
serviceDetails.SetIPv4(ip)
c.service.RegisterRemoteSKI(ski)
c.mux.Lock()
defer c.mux.Unlock()
c.clients[ski] = EEBusClientCBs{onConnect: connectHandler, onDisconnect: disconnectHandler}
c.clients[ski] = device
return c.Cem.RegisterEmobilityRemoteDevice(serviceDetails, dataProvider)
return c.evseUC
}
func (c *EEBus) Run() {
c.Cem.Start()
c.service.Start()
}
func (c *EEBus) Shutdown() {
c.Cem.Shutdown()
c.service.Shutdown()
}
// EVSE/EV UseCase CB
func (c *EEBus) evseUsecaseCB(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) {
c.mux.Lock()
defer c.mux.Unlock()
if client, ok := c.clients[ski]; ok {
client.UseCaseEventCB(device, entity, event)
}
}
// EEBUSServiceHandler
// report the Ship ID of a newly trusted connection
func (c *EEBus) RemoteServiceShipIDReported(service *service.EEBUSService, ski string, shipID string) {
// we should associated the Ship ID with the SKI and store it
// so the next connection can start trusted
c.log.DEBUG.Println("SKI", ski, "has Ship ID:", shipID)
}
func (c *EEBus) RemoteSKIConnected(service *service.EEBUSService, ski string) {
// no implementation needed, handled in CEM events
func (c *EEBus) RemoteSKIConnected(service eebusapi.ServiceInterface, ski string) {
c.mux.Lock()
defer c.mux.Unlock()
client, exists := c.clients[ski]
if !exists {
return
if client, ok := c.clients[ski]; ok {
client.DeviceConnect()
}
client.onConnect(ski)
}
func (c *EEBus) RemoteSKIDisconnected(service *service.EEBUSService, ski string) {
// no implementation needed, handled in CEM events
func (c *EEBus) RemoteSKIDisconnected(service eebusapi.ServiceInterface, ski string) {
c.mux.Lock()
defer c.mux.Unlock()
client, exists := c.clients[ski]
if !exists {
return
if client, ok := c.clients[ski]; ok {
client.DeviceConnect()
}
client.onDisconnect(ski)
}
func (h *EEBus) ReportServiceShipID(ski string, shipdID string) {}
// report all currently visible EEBUS services
// this is needed to provide an UI for pairing with other devices
// if not all incoming pairing requests should be accepted
func (c *EEBus) VisibleRemoteServicesUpdated(service eebusapi.ServiceInterface, entries []shipapi.RemoteService) {
}
// Provides the SHIP ID the remote service reported during the handshake process
// This needs to be persisted and passed on for future remote service connections
// when using `PairRemoteService`
func (c *EEBus) ServiceShipIDUpdate(ski string, shipdID string) {}
// Provides the current pairing state for the remote service
// This is called whenever the state changes and can be used to
// provide user information for the pairing/connection process
func (c *EEBus) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) {
if detail.State() != shipapi.ConnectionStateReceivedPairingRequest {
return
}
c.mux.Lock()
defer c.mux.Unlock()
if _, ok := c.clients[ski]; !ok {
// this is an unknown SKI, so deny pairing
c.service.CancelPairingWithSKI(ski)
}
}
// EEBUS Logging interface
@ -228,68 +287,3 @@ func (c *EEBus) Error(args ...interface{}) {
func (c *EEBus) Errorf(format string, args ...interface{}) {
c.log.ERROR.Printf(format, args...)
}
// Certificate helpers
// CreateCertificate returns a newly created EEBUS compatible certificate
func CreateCertificate() (tls.Certificate, error) {
return service.CreateCertificate("", EEBUSBrandName, "DE", EEBUSDeviceCode)
}
// pemBlockForKey marshals private key into pem block
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil, fmt.Errorf("unable to marshal ECDSA private key: %w", err)
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
default:
return nil, errors.New("unknown private key type")
}
}
// GetX509KeyPair saves returns the cert and key string values
func GetX509KeyPair(cert tls.Certificate) (string, string, error) {
var certValue, keyValue string
out := new(bytes.Buffer)
err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]})
if err == nil {
certValue = out.String()
}
if len(certValue) > 0 {
var pb *pem.Block
if pb, err = pemBlockForKey(cert.PrivateKey); err == nil {
out.Reset()
err = pem.Encode(out, pb)
}
}
if err == nil {
keyValue = out.String()
}
return certValue, keyValue, err
}
// SkiFromX509 extracts SKI from certificate
func skiFromX509(leaf *x509.Certificate) (string, error) {
if len(leaf.SubjectKeyId) == 0 {
return "", errors.New("missing SubjectKeyId")
}
return fmt.Sprintf("%0x", leaf.SubjectKeyId), nil
}
// SkiFromCert extracts SKI from certificate
func SkiFromCert(cert tls.Certificate) (string, error) {
leaf, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return "", errors.New("failed parsing certificate: " + err.Error())
}
return skiFromX509(leaf)
}

View file

@ -3,6 +3,9 @@ package charger
import (
"testing"
"github.com/enbility/eebus-go/usecases/mocks"
spinemocks "github.com/enbility/spine-go/mocks"
"github.com/evcc-io/evcc/charger/eebus"
"go.uber.org/mock/gomock"
)
@ -116,9 +119,19 @@ func TestEEBusIsCharging(t *testing.T) {
for index, m := range tc.measurements {
ctrl := gomock.NewController(t)
emobilityMock := NewMockEmobilityI(ctrl)
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{
emobility: emobilityMock,
uc: uc,
ev: evEntity,
}
currents := make([]float64, 0)
@ -127,8 +140,9 @@ func TestEEBusIsCharging(t *testing.T) {
currents = append(currents, d.current)
}
emobilityMock.EXPECT().EVCurrentsPerPhase().Return(currents, nil).AnyTimes()
emobilityMock.EXPECT().EVCurrentLimits().Return(limitsMin, limitsMax, limitsDefault, nil)
evcc.EXPECT().EVConnected(evEntity).Return(true)
evcem.EXPECT().CurrentPerPhase(evEntity).Return(currents, nil)
opev.EXPECT().CurrentLimits(evEntity).Return(limitsMin, limitsMax, limitsDefault, nil)
result := eebus.isCharging()
if result != m.expected {

View file

@ -1,350 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/enbility/cemd/emobility (interfaces: EmobilityI)
//
// Generated by this command:
//
// mockgen -package charger -destination eebus_test_mock.go github.com/enbility/cemd/emobility EmobilityI
//
// Package charger is a generated GoMock package.
package charger
import (
reflect "reflect"
emobility "github.com/enbility/cemd/emobility"
gomock "go.uber.org/mock/gomock"
)
// MockEmobilityI is a mock of EmobilityI interface.
type MockEmobilityI struct {
ctrl *gomock.Controller
recorder *MockEmobilityIMockRecorder
}
// MockEmobilityIMockRecorder is the mock recorder for MockEmobilityI.
type MockEmobilityIMockRecorder struct {
mock *MockEmobilityI
}
// NewMockEmobilityI creates a new mock instance.
func NewMockEmobilityI(ctrl *gomock.Controller) *MockEmobilityI {
mock := &MockEmobilityI{ctrl: ctrl}
mock.recorder = &MockEmobilityIMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockEmobilityI) EXPECT() *MockEmobilityIMockRecorder {
return m.recorder
}
// EVChargeStrategy mocks base method.
func (m *MockEmobilityI) EVChargeStrategy() emobility.EVChargeStrategyType {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVChargeStrategy")
ret0, _ := ret[0].(emobility.EVChargeStrategyType)
return ret0
}
// EVChargeStrategy indicates an expected call of EVChargeStrategy.
func (mr *MockEmobilityIMockRecorder) EVChargeStrategy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVChargeStrategy", reflect.TypeOf((*MockEmobilityI)(nil).EVChargeStrategy))
}
// EVChargedEnergy mocks base method.
func (m *MockEmobilityI) EVChargedEnergy() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVChargedEnergy")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVChargedEnergy indicates an expected call of EVChargedEnergy.
func (mr *MockEmobilityIMockRecorder) EVChargedEnergy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVChargedEnergy", reflect.TypeOf((*MockEmobilityI)(nil).EVChargedEnergy))
}
// EVCommunicationStandard mocks base method.
func (m *MockEmobilityI) EVCommunicationStandard() (emobility.EVCommunicationStandardType, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVCommunicationStandard")
ret0, _ := ret[0].(emobility.EVCommunicationStandardType)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVCommunicationStandard indicates an expected call of EVCommunicationStandard.
func (mr *MockEmobilityIMockRecorder) EVCommunicationStandard() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCommunicationStandard", reflect.TypeOf((*MockEmobilityI)(nil).EVCommunicationStandard))
}
// EVConnected mocks base method.
func (m *MockEmobilityI) EVConnected() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVConnected")
ret0, _ := ret[0].(bool)
return ret0
}
// EVConnected indicates an expected call of EVConnected.
func (mr *MockEmobilityIMockRecorder) EVConnected() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVConnected", reflect.TypeOf((*MockEmobilityI)(nil).EVConnected))
}
// EVConnectedPhases mocks base method.
func (m *MockEmobilityI) EVConnectedPhases() (uint, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVConnectedPhases")
ret0, _ := ret[0].(uint)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVConnectedPhases indicates an expected call of EVConnectedPhases.
func (mr *MockEmobilityIMockRecorder) EVConnectedPhases() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVConnectedPhases", reflect.TypeOf((*MockEmobilityI)(nil).EVConnectedPhases))
}
// EVCoordinatedChargingSupported mocks base method.
func (m *MockEmobilityI) EVCoordinatedChargingSupported() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVCoordinatedChargingSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVCoordinatedChargingSupported indicates an expected call of EVCoordinatedChargingSupported.
func (mr *MockEmobilityIMockRecorder) EVCoordinatedChargingSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCoordinatedChargingSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVCoordinatedChargingSupported))
}
// EVCurrentChargeState mocks base method.
func (m *MockEmobilityI) EVCurrentChargeState() (emobility.EVChargeStateType, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVCurrentChargeState")
ret0, _ := ret[0].(emobility.EVChargeStateType)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVCurrentChargeState indicates an expected call of EVCurrentChargeState.
func (mr *MockEmobilityIMockRecorder) EVCurrentChargeState() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentChargeState", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentChargeState))
}
// EVCurrentLimits mocks base method.
func (m *MockEmobilityI) EVCurrentLimits() ([]float64, []float64, []float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVCurrentLimits")
ret0, _ := ret[0].([]float64)
ret1, _ := ret[1].([]float64)
ret2, _ := ret[2].([]float64)
ret3, _ := ret[3].(error)
return ret0, ret1, ret2, ret3
}
// EVCurrentLimits indicates an expected call of EVCurrentLimits.
func (mr *MockEmobilityIMockRecorder) EVCurrentLimits() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentLimits))
}
// EVCurrentsPerPhase mocks base method.
func (m *MockEmobilityI) EVCurrentsPerPhase() ([]float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVCurrentsPerPhase")
ret0, _ := ret[0].([]float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVCurrentsPerPhase indicates an expected call of EVCurrentsPerPhase.
func (mr *MockEmobilityIMockRecorder) EVCurrentsPerPhase() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentsPerPhase", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentsPerPhase))
}
// EVEnergyDemand mocks base method.
func (m *MockEmobilityI) EVEnergyDemand() (emobility.EVDemand, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVEnergyDemand")
ret0, _ := ret[0].(emobility.EVDemand)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVEnergyDemand indicates an expected call of EVEnergyDemand.
func (mr *MockEmobilityIMockRecorder) EVEnergyDemand() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVEnergyDemand", reflect.TypeOf((*MockEmobilityI)(nil).EVEnergyDemand))
}
// EVGetIncentiveConstraints mocks base method.
func (m *MockEmobilityI) EVGetIncentiveConstraints() emobility.EVIncentiveSlotConstraints {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVGetIncentiveConstraints")
ret0, _ := ret[0].(emobility.EVIncentiveSlotConstraints)
return ret0
}
// EVGetIncentiveConstraints indicates an expected call of EVGetIncentiveConstraints.
func (mr *MockEmobilityIMockRecorder) EVGetIncentiveConstraints() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVGetIncentiveConstraints", reflect.TypeOf((*MockEmobilityI)(nil).EVGetIncentiveConstraints))
}
// EVGetPowerConstraints mocks base method.
func (m *MockEmobilityI) EVGetPowerConstraints() emobility.EVTimeSlotConstraints {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVGetPowerConstraints")
ret0, _ := ret[0].(emobility.EVTimeSlotConstraints)
return ret0
}
// EVGetPowerConstraints indicates an expected call of EVGetPowerConstraints.
func (mr *MockEmobilityIMockRecorder) EVGetPowerConstraints() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVGetPowerConstraints", reflect.TypeOf((*MockEmobilityI)(nil).EVGetPowerConstraints))
}
// EVIdentification mocks base method.
func (m *MockEmobilityI) EVIdentification() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVIdentification")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVIdentification indicates an expected call of EVIdentification.
func (mr *MockEmobilityIMockRecorder) EVIdentification() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVIdentification", reflect.TypeOf((*MockEmobilityI)(nil).EVIdentification))
}
// EVLoadControlObligationLimits mocks base method.
func (m *MockEmobilityI) EVLoadControlObligationLimits() ([]float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVLoadControlObligationLimits")
ret0, _ := ret[0].([]float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVLoadControlObligationLimits indicates an expected call of EVLoadControlObligationLimits.
func (mr *MockEmobilityIMockRecorder) EVLoadControlObligationLimits() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVLoadControlObligationLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVLoadControlObligationLimits))
}
// EVOptimizationOfSelfConsumptionSupported mocks base method.
func (m *MockEmobilityI) EVOptimizationOfSelfConsumptionSupported() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVOptimizationOfSelfConsumptionSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVOptimizationOfSelfConsumptionSupported indicates an expected call of EVOptimizationOfSelfConsumptionSupported.
func (mr *MockEmobilityIMockRecorder) EVOptimizationOfSelfConsumptionSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVOptimizationOfSelfConsumptionSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVOptimizationOfSelfConsumptionSupported))
}
// EVPowerPerPhase mocks base method.
func (m *MockEmobilityI) EVPowerPerPhase() ([]float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVPowerPerPhase")
ret0, _ := ret[0].([]float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVPowerPerPhase indicates an expected call of EVPowerPerPhase.
func (mr *MockEmobilityIMockRecorder) EVPowerPerPhase() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVPowerPerPhase", reflect.TypeOf((*MockEmobilityI)(nil).EVPowerPerPhase))
}
// EVSoC mocks base method.
func (m *MockEmobilityI) EVSoC() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVSoC")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVSoC indicates an expected call of EVSoC.
func (mr *MockEmobilityIMockRecorder) EVSoC() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVSoC", reflect.TypeOf((*MockEmobilityI)(nil).EVSoC))
}
// EVSoCSupported mocks base method.
func (m *MockEmobilityI) EVSoCSupported() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVSoCSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EVSoCSupported indicates an expected call of EVSoCSupported.
func (mr *MockEmobilityIMockRecorder) EVSoCSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVSoCSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVSoCSupported))
}
// EVWriteIncentives mocks base method.
func (m *MockEmobilityI) EVWriteIncentives(arg0 []emobility.EVDurationSlotValue) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVWriteIncentives", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// EVWriteIncentives indicates an expected call of EVWriteIncentives.
func (mr *MockEmobilityIMockRecorder) EVWriteIncentives(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWriteIncentives", reflect.TypeOf((*MockEmobilityI)(nil).EVWriteIncentives), arg0)
}
// EVWriteLoadControlLimits mocks base method.
func (m *MockEmobilityI) EVWriteLoadControlLimits(arg0, arg1 []float64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVWriteLoadControlLimits", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// EVWriteLoadControlLimits indicates an expected call of EVWriteLoadControlLimits.
func (mr *MockEmobilityIMockRecorder) EVWriteLoadControlLimits(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWriteLoadControlLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVWriteLoadControlLimits), arg0, arg1)
}
// EVWritePowerLimits mocks base method.
func (m *MockEmobilityI) EVWritePowerLimits(arg0 []emobility.EVDurationSlotValue) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EVWritePowerLimits", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// EVWritePowerLimits indicates an expected call of EVWritePowerLimits.
func (mr *MockEmobilityIMockRecorder) EVWritePowerLimits(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWritePowerLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVWritePowerLimits), arg0)
}

13
go.mod
View file

@ -24,8 +24,9 @@ require (
github.com/dmarkham/enumer v1.5.10
github.com/dylanmei/iso8601 v0.1.0
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/enbility/cemd v0.5.0
github.com/enbility/eebus-go v0.5.0
github.com/enbility/eebus-go v0.6.0
github.com/enbility/ship-go v0.5.1
github.com/enbility/spine-go v0.6.0
github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701
github.com/fatih/structs v1.1.0
github.com/glebarez/sqlite v1.11.0
@ -119,6 +120,7 @@ require (
github.com/cstockton/go-conv v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
@ -131,7 +133,7 @@ require (
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golanguzb70/lrucache v1.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
@ -174,6 +176,7 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/teivah/onecontext v1.3.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
@ -193,7 +196,3 @@ require (
)
replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467
replace github.com/enbility/cemd => github.com/enbility/cemd v0.2.2
replace github.com/enbility/eebus-go => github.com/enbility/eebus-go v0.2.0

22
go.sum
View file

@ -121,10 +121,14 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/enbility/cemd v0.2.2 h1:NrN7DCxv7C6YD5CaYgiebS/iPA3QmQeugF/hvNFKHNA=
github.com/enbility/cemd v0.2.2/go.mod h1:BZoHbJQJ9/7le4WMFAJWRSgKCCTfNVEOM0c1E3H1JxE=
github.com/enbility/eebus-go v0.2.0 h1:znQUfG1QYk0Q+vOacrsSNtXmitF1F2Rx9+ohwcRNlRw=
github.com/enbility/eebus-go v0.2.0/go.mod h1:Ozg1eDUfSbHfQ1dWfyAUa3h8dMtgM/01eO30kHca5zk=
github.com/enbility/eebus-go v0.6.0 h1:TiX397ON3bCPnn49BqkzEd+GTWI4rj0USmRuWxbdr5s=
github.com/enbility/eebus-go v0.6.0/go.mod h1:6ka3OsfqJDiXAWMMYO1LkKQa7xSIgqrhwOeP5kO/yao=
github.com/enbility/ship-go v0.5.1 h1:8Vax1MpyI/C+kQlMFAzQ7/h/xJ7fuumSLJy9FYgEkcE=
github.com/enbility/ship-go v0.5.1/go.mod h1:jewJWYQ10jNhsnhS1C4jESx3CNmDa5HNWZjBhkTug5Y=
github.com/enbility/spine-go v0.6.0 h1:sd5x9ZckvhI9b07HrdxKEZAcpF1Jt+xMC46o3VpuwoY=
github.com/enbility/spine-go v0.6.0/go.mod h1:1SJ6+ihCkjzVALXXbB3xdjDGp2ITqq9gnJQiPsF3fb0=
github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b h1:sg3c6LJ4eWffwtt9SW0lgcIX4Oh274vwdJnNFNNrDco=
github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b/go.mod h1:BjzRRiYX6mWdOgku1xxDE+NsV8PijTby7Q7BkYVdfDU=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@ -214,8 +218,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -229,6 +231,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golanguzb70/lrucache v1.2.0 h1:VjpjmB4VTf9VXBtZTJGcgcN0CNFM5egDrrSjkGyQOlg=
github.com/golanguzb70/lrucache v1.2.0/go.mod h1:zc2GD26KwGEDdTHsCCTcJorv/11HyKwQVS9gqg2bizc=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -664,7 +668,6 @@ github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iL
github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/bboehmke/sunny v0.16.0 h1:arcU5MNupJ9KELOcSC82mYPGP/friBop+PxtybhqRwo=
gitlab.com/bboehmke/sunny v0.16.0/go.mod h1:F5AIuL7kYteSJFR5E+YEocxIdpyCXmtDciFmMQVjP88=
@ -712,7 +715,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
@ -738,7 +740,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@ -794,10 +795,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -846,7 +845,6 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=

View file

@ -11,3 +11,4 @@ params:
render: |
{{ include "eebus" . }}
meter: true
vasvw: true