diff --git a/charger/eebus.go b/charger/eebus.go index 7b855d7bc8..c78616d6be 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -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() + evEntity := c.evEntity() + if !c.uc.EvCC.EVConnected(evEntity) { + return errors.New("no ev connected") + } + + _, maxLimits, _, err := c.uc.OpEV.CurrentLimits(evEntity) if err != nil { - return err + return errors.New("no limits available") } - // 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] - } + // 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 } diff --git a/charger/eebus/certificate.go b/charger/eebus/certificate.go new file mode 100644 index 0000000000..7364aa7ab8 --- /dev/null +++ b/charger/eebus/certificate.go @@ -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) +} diff --git a/charger/eebus/eebus.go b/charger/eebus/eebus.go index 5e9d601c4e..bb80814d2b 100644 --- a/charger/eebus/eebus.go +++ b/charger/eebus/eebus.go @@ -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() } -// EEBUSServiceHandler +// 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() -// 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) + if client, ok := c.clients[ski]; ok { + client.UseCaseEventCB(device, entity, event) + } } -func (c *EEBus) RemoteSKIConnected(service *service.EEBUSService, ski string) { +// EEBUSServiceHandler + +// 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) -} diff --git a/charger/eebus_test.go b/charger/eebus_test.go index 68570b2917..d352eeef03 100644 --- a/charger/eebus_test.go +++ b/charger/eebus_test.go @@ -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 { diff --git a/charger/eebus_test_mock.go b/charger/eebus_test_mock.go deleted file mode 100644 index bc42eaaeb5..0000000000 --- a/charger/eebus_test_mock.go +++ /dev/null @@ -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) -} diff --git a/go.mod b/go.mod index 6f3e3a4bab..946494bb11 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index eadea95f79..3159dccb05 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/templates/definition/charger/porsche-pmcc.yaml b/templates/definition/charger/porsche-pmcc.yaml index 9d1c84b3ec..05a766b822 100644 --- a/templates/definition/charger/porsche-pmcc.yaml +++ b/templates/definition/charger/porsche-pmcc.yaml @@ -11,3 +11,4 @@ params: render: | {{ include "eebus" . }} meter: true + vasvw: true