Migrate EEBUS to enbility/eebus-go (#5391)

* Migrate EEBUS to enbility/eebus-go

- adopt enbility/eebus-go library
- fully compatible with existing EEBUS setup
- centralize EEBUS certificate generation for configure and certificate creation cmds
- automatically utilizes native avahi mDNS setup if installed, which does not have missing IP issues as zeroconf library
- mDNS discovery and announcement is fully handled by the library
- improved double connection and reconnection handling over previous library
- logging now fully supported evcc log levels
- tested with Elli Connect and Porsche Mobile Charger Connect
This commit is contained in:
Andreas Linde 2022-12-10 21:40:20 +01:00 • committed by GitHub
parent e3dc7986a0
commit ae393838f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 452 additions and 741 deletions

View file

@ -5,9 +5,7 @@ import (
"fmt"
"time"
"github.com/evcc-io/eebus/app"
"github.com/evcc-io/eebus/communication"
"github.com/evcc-io/eebus/ship"
"github.com/enbility/cemd/emobility"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/server"
@ -15,18 +13,19 @@ import (
)
const (
maxIdRequestTimespan = time.Second * 120
idleFactor = 0.6
maxIdRequestTimespan = time.Second * 120
idleFactor = 0.6
voltage float64 = 230
)
type EEBus struct {
ski string
emobility emobility.EmobilityI
log *util.Logger
cc *communication.ConnectionController
lp loadpoint.API
communicationStandard communication.EVCommunicationStandardEnumType
socSupportAvailable bool
selfConsumptionSupportAvailable bool
communicationStandard emobility.EVCommunicationStandardType
maxCurrent float64
connected bool
@ -71,11 +70,12 @@ func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy bool) (api.Charger, err
}
c := &EEBus{
ski: ski,
log: log,
communicationStandard: communication.EVCommunicationStandardEnumTypeUnknown,
communicationStandard: emobility.EVCommunicationStandardTypeUnknown,
}
server.EEBusInstance.Register(ski, ip, c.onConnect, c.onDisconnect)
c.emobility = server.EEBusInstance.Register(ski, ip, c.onConnect, c.onDisconnect)
if hasMeter {
if hasChargedEnergy {
@ -87,34 +87,24 @@ func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy bool) (api.Charger, err
return c, nil
}
func (c *EEBus) onConnect(ski string, conn ship.Conn) error {
func (c *EEBus) onConnect(ski string) {
c.log.TRACE.Println("!! onConnect invoked on ski ", ski)
eebusDevice := app.HEMS(server.EEBusInstance.DeviceInfo())
c.cc = communication.NewConnectionController(c.log.TRACE, conn, eebusDevice)
c.cc.SetDataUpdateHandler(c.dataUpdateHandler)
c.cc.Voltage = 230.0 // TODO value should be provided from site
c.setDefaultValues()
c.setConnected(true)
err := c.cc.Boot()
return err
}
func (c *EEBus) onDisconnect(ski string) {
c.log.TRACE.Println("!! onDisconnect invoked on ski ", ski)
c.expectedEnableState = false
c.setConnected(false)
c.setDefaultValues()
}
func (c *EEBus) setDefaultValues() {
c.expectedEnableState = false
c.communicationStandard = communication.EVCommunicationStandardEnumTypeUnknown
c.socSupportAvailable = false
c.selfConsumptionSupportAvailable = false
c.communicationStandard = emobility.EVCommunicationStandardTypeUnknown
c.lastIsChargingCheck = time.Now().Add(-time.Hour * 1)
c.lastIsChargingResult = false
}
@ -126,17 +116,18 @@ func (c *EEBus) setConnected(connected bool) {
c.connected = connected
}
func (c *EEBus) setLoadpointMinMaxLimits(data *communication.EVSEClientDataType) {
func (c *EEBus) setLoadpointMinMaxLimits() {
if c.lp == nil {
return
}
if len(data.EVData.Limits) == 0 {
minLimits, maxLimits, _, err := c.emobility.EVCurrentLimits()
if err != nil || len(minLimits) == 0 || len(maxLimits) == 0 {
return
}
newMin := data.EVData.Limits[1].Min
newMax := data.EVData.Limits[1].Max
newMin := minLimits[0]
newMax := maxLimits[0]
if c.lp.GetMinCurrent() != newMin && newMin > 0 {
c.lp.SetMinCurrent(newMin)
@ -146,84 +137,15 @@ func (c *EEBus) setLoadpointMinMaxLimits(data *communication.EVSEClientDataType)
}
}
func (c *EEBus) showCurrentChargingSetup() {
data, err := c.cc.GetData()
if err != nil {
return
}
prevComStandard := c.communicationStandard
prevSoCSupport := c.socSupportAvailable
prevSelfConsumptionSupport := c.selfConsumptionSupportAvailable
if prevComStandard != data.EVData.CommunicationStandard {
c.communicationStandard = data.EVData.CommunicationStandard
c.log.TRACE.Println("ev-charger-communication changed from ", prevComStandard, " to ", data.EVData.CommunicationStandard)
}
if prevSoCSupport != data.EVData.UCSoCAvailable {
c.socSupportAvailable = data.EVData.UCSoCAvailable
c.log.TRACE.Println("ev-charger-soc support changed from ", prevSoCSupport, " to ", data.EVData.UCSoCAvailable)
}
if prevSelfConsumptionSupport != data.EVData.UCSelfConsumptionAvailable {
c.selfConsumptionSupportAvailable = data.EVData.UCSelfConsumptionAvailable
c.log.TRACE.Println("ev-charger-self-consumption-support support changed from ", prevSelfConsumptionSupport, " to ", data.EVData.UCSelfConsumptionAvailable)
}
}
func (c *EEBus) dataUpdateHandler(dataType communication.EVDataElementUpdateType, data *communication.EVSEClientDataType) {
// we receive data, so it is connected
c.setConnected(true)
prevSelfConsumptionSupport := c.selfConsumptionSupportAvailable
c.showCurrentChargingSetup()
switch dataType {
case communication.EVDataElementUpdateUseCaseSelfConsumption:
// if availability of self consumption use case changes, resend the current charging limit
// but only if the support value actually changed
if prevSelfConsumptionSupport != c.selfConsumptionSupportAvailable {
if err := c.writeCurrentLimitData([]float64{c.maxCurrent, c.maxCurrent, c.maxCurrent}); err != nil {
c.log.WARN.Println("failed to send current limit data: ", err)
}
}
// case communication.EVDataElementUpdateUseCaseSoC:
case communication.EVDataElementUpdateEVConnectionState:
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged {
c.expectedEnableState = false
}
c.setLoadpointMinMaxLimits(data)
case communication.EVDataElementUpdateCommunicationStandard:
c.communicationStandard = data.EVData.CommunicationStandard
c.setLoadpointMinMaxLimits(data)
case communication.EVDataElementUpdateAsymetricChargingType:
c.setLoadpointMinMaxLimits(data)
// case communication.EVDataElementUpdateEVSEOperationState:
// case communication.EVDataElementUpdateEVChargeState:
// case communication.EVDataElementUpdateChargingStrategy:
case communication.EVDataElementUpdateChargingPlanRequired:
if err := c.writeChargingPlan(); err != nil {
c.log.INFO.Println("failed to send charging plan: ", err)
}
case communication.EVDataElementUpdateConnectedPhases:
c.setLoadpointMinMaxLimits(data)
case communication.EVDataElementUpdatePowerLimits:
c.setLoadpointMinMaxLimits(data)
case communication.EVDataElementUpdateAmperageLimits:
c.setLoadpointMinMaxLimits(data)
}
}
// we assume that if any phase current value is > idleFactor * min Current, then charging is active and enabled is true
func (c *EEBus) isCharging(d *communication.EVSEClientDataType) bool {
func (c *EEBus) isCharging() bool { // d *communication.EVSEClientDataType
// check if an external physical meter is assigned
// we only want this for configured meters and not for internal meters!
// right now it works as expected
if c.lp != nil && c.lp.HasChargeMeter() {
// we only check ever 10 seconds, maybe we can use the config interval duration
timeDiff := time.Since(c.lastIsChargingCheck)
if timeDiff.Seconds() >= 10.0 {
if timeDiff.Seconds() >= 10 {
c.lastIsChargingCheck = time.Now()
c.lastIsChargingResult = false
if c.lp.GetChargePower() > c.lp.GetMinPower()*idleFactor {
@ -236,14 +158,22 @@ func (c *EEBus) isCharging(d *communication.EVSEClientDataType) bool {
}
// The above doesn't (yet) work for built in meters, so check the EEBUS measurements also
var phase uint
for phase = 1; phase <= d.EVData.ConnectedPhases; phase++ {
if phaseCurrent, ok := d.EVData.Measurements.Current.Load(phase); ok {
if _, ok := phaseCurrent.(float64); ok {
if phaseCurrent.(float64) > d.EVData.Limits[phase].Min*idleFactor {
return true
}
}
currents, err := c.emobility.EVCurrentsPerPhase()
if err != nil {
return false
}
limitsMin, _, _, err := c.emobility.EVCurrentLimits()
if err != nil {
return false
}
for index, phaseCurrent := range currents {
if len(limitsMin) <= index {
break
}
limitMin := limitsMin[index]
if phaseCurrent > limitMin*idleFactor {
return true
}
}
@ -251,35 +181,33 @@ func (c *EEBus) isCharging(d *communication.EVSEClientDataType) bool {
}
func (c *EEBus) updateState() (api.ChargeStatus, error) {
data, err := c.cc.GetData()
currentState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return api.StatusNone, err
}
currentState := data.EVData.ChargeState
if !c.connected {
return api.StatusNone, fmt.Errorf("charger reported as disconnected")
return api.StatusNone, fmt.Errorf("%s charger reported as disconnected", c.ski)
}
switch currentState {
case communication.EVChargeStateEnumTypeUnknown, communication.EVChargeStateEnumTypeUnplugged: // Unplugged
case emobility.EVChargeStateTypeUnknown, emobility.EVChargeStateTypeUnplugged: // Unplugged
c.expectedEnableState = false
return api.StatusA, nil
case communication.EVChargeStateEnumTypeFinished, communication.EVChargeStateEnumTypePaused: // Finished, Paused
case emobility.EVChargeStateTypeFinished, emobility.EVChargeStateTypePaused: // Finished, Paused
return api.StatusB, nil
case communication.EVChargeStateEnumTypeActive: // Active
if c.isCharging(data) {
case emobility.EVChargeStateTypeActive: // Active
if c.isCharging() {
// we might already be enabled and charging due to connection issues
c.expectedEnableState = true
return api.StatusC, nil
}
return api.StatusB, nil
case communication.EVChargeStateEnumTypeError: // Error
case emobility.EVChargeStateTypeError: // Error
return api.StatusF, nil
}
return api.StatusNone, fmt.Errorf("properties unknown result: %s", currentState)
return api.StatusNone, fmt.Errorf("%s properties unknown result: %s", c.ski, currentState)
}
// Status implements the api.Charger interface
@ -296,12 +224,8 @@ func (c *EEBus) Enabled() (bool, error) {
// Enable implements the api.Charger interface
func (c *EEBus) Enable(enable bool) error {
data, err := c.cc.GetData()
if err != nil {
return err
}
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged {
currentState, err := c.emobility.EVCurrentChargeState()
if err != nil || currentState == emobility.EVChargeStateTypeUnplugged {
// if the ev is unplugged, we do not need to disable charging by setting a current of 0 as it already is
if !enable {
return nil
@ -312,7 +236,14 @@ 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 data.EVData.CommunicationStandard == communication.EVCommunicationStandardEnumTypeUnknown {
comStandard, err := c.emobility.EVCommunicationStandard()
if err != nil || comStandard == emobility.EVCommunicationStandardTypeUnknown {
return api.ErrMustRetry
}
// we have to know the limits
minLimits, maxLimits, _, err := c.emobility.EVCurrentLimits()
if err != nil {
return api.ErrMustRetry
}
@ -328,7 +259,7 @@ func (c *EEBus) Enable(enable bool) error {
// switching between 1/3 phases: stop charging, pause for 2 minutes, change phases, resume charging
// frequent switching should be avoided by all means!
c.maxCurrent = 0
return c.writeCurrentLimitData([]float64{0.0, 0.0, 0.0})
return c.writeCurrentLimitData([]float64{0, 0, 0})
}
// if we set MaxCurrent > Min value and then try to enable the charger, it would reset it to min
@ -338,147 +269,17 @@ func (c *EEBus) Enable(enable bool) error {
// we need to check if the mode is set to now as the currents won't be adjusted afterwards any more in all cases
if c.lp.GetMode() == api.ModeNow {
return c.writeCurrentLimitData([]float64{data.EVData.Limits[1].Max, data.EVData.Limits[2].Max, data.EVData.Limits[3].Max})
return c.writeCurrentLimitData(maxLimits)
}
// in non now mode only enable with min settings, so we don't excessively consume power in case it has to be turned of in the next cycle anyways
return c.writeCurrentLimitData([]float64{data.EVData.Limits[1].Min, data.EVData.Limits[2].Min, data.EVData.Limits[3].Min})
}
// returns true if the connected EV supports charging recommendation
func (c *EEBus) optimizationSelfConsumptionAvailable() bool {
data, err := c.cc.GetData()
if err == nil {
return data.EVData.UCSelfConsumptionAvailable
}
return false
}
// respond to a charging plan request from the EV
func (c *EEBus) writeChargingPlan() error {
data, err := c.cc.GetData()
if err != nil {
return err
}
var chargingPlan communication.EVChargingPlan
tariffGrid := 0.30
tariffFeedIn := 0.10
maxPower := c.lp.GetMaxPower()
switch data.EVData.ChargingStrategy {
case communication.EVChargingStrategyEnumTypeNoDemand, communication.EVChargingStrategyEnumTypeUnknown:
// The EV has no power demand or we don't know it yet, so we shouldn't get here
// TODO: why did we get here?
// lets do 24 1 hour slots with maximum power, power will be adjusted via Overload Protection limits
for i := 0; i < 24; i++ {
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: time.Hour,
MaxValue: maxPower,
Pricing: tariffGrid,
})
}
chargingPlan.Duration = 24 * time.Hour
case communication.EVChargingStrategyEnumTypeDirectCharging:
// The EV is in direct charging mode
// Does it support self consumption?
if c.optimizationSelfConsumptionAvailable() {
// this should mean that any mode in evcc is ignored and the EV is in full control
// TODO: is this the right approach?
// lets do one 24 hour slot with maximum power, power will be adjusted via Overload Protection limits
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: time.Duration(24) * time.Hour,
MaxValue: maxPower,
Pricing: tariffGrid,
})
chargingPlan.Duration = 24 * time.Hour
} else {
// in this mode we need to enforce the evcc modes
// we need to create a 24h charging plan
chargingPlan.Duration = 24 * time.Hour
currentMode := c.lp.GetMode()
switch currentMode {
case api.ModeNow, api.ModeMinPV:
// lets do one 24 hour slot with maximum power, power will be adjusted via Overload Protection limits
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: time.Duration(24) * time.Hour,
MaxValue: maxPower,
Pricing: tariffGrid,
})
chargingPlan.Duration = 24 * time.Hour
case api.ModePV:
// lets do 24 1 hour slots with maximum power, power will be adjusted via Overload Protection limits
// but set the nightly hours to 0 W, we assume those to be from 20:00 to 07:00
now := time.Now()
for i := 0; i < 24; i++ {
power := maxPower
pricing := tariffFeedIn
if now.Hour()+i >= 20 || now.Hour()+i < 7 {
power = 0.0
pricing = tariffGrid
}
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: time.Hour,
MaxValue: power,
Pricing: pricing,
})
}
chargingPlan.Duration = 24 * time.Hour
case api.ModeOff:
// lets do 24 1 hour slots with 0 W, so it wakes at once an hour to check back
for i := 0; i < 24; i++ {
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: time.Hour,
MaxValue: 0,
Pricing: tariffGrid,
})
}
chargingPlan.Duration = 24 * time.Hour
}
}
case communication.EVChargingStrategyEnumTypeTimedCharging:
// The EV is in timed charging mode
targetDuration := data.EVData.ChargingTargetDuration
// split the duration into full hours, with the remaining time at the start
hours := int(targetDuration.Hours())
remainingDuration := targetDuration - (time.Duration(hours) * time.Hour)
if remainingDuration > 0 {
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: remainingDuration,
MaxValue: maxPower,
Pricing: tariffGrid,
})
}
for i := 0; i < hours; i++ {
chargingPlan.Slots = append(chargingPlan.Slots, communication.EVChargingSlot{
Duration: time.Hour,
MaxValue: maxPower,
Pricing: tariffGrid,
})
}
chargingPlan.Duration = targetDuration
default:
return fmt.Errorf("charging strategy not implemented: %s", data.EVData.ChargingStrategy)
}
return c.cc.WriteChargingPlan(chargingPlan)
return c.writeCurrentLimitData(minLimits)
}
// send current charging power limits to the EV
func (c *EEBus) writeCurrentLimitData(currents []float64) error {
data, err := c.cc.GetData()
comStandard, err := c.emobility.EVCommunicationStandard()
if err != nil {
return err
}
@ -489,12 +290,12 @@ func (c *EEBus) writeCurrentLimitData(currents []float64) error {
// wait for the car to go into sleep and plug it back in.
// So if are currentls smaller 6A with unknown communication standard change them to 6A
// keep in mind, that still will confuse evcc as it thinks charging is stopped, but it isn't yet
if data.EVData.CommunicationStandard == communication.EVCommunicationStandardEnumTypeUnknown {
for index, current := range currents {
phase := uint(index) + 1
if limit, ok := data.EVData.Limits[phase]; ok {
if current < limit.Min {
currents[index] = limit.Min
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]
}
}
}
@ -502,7 +303,7 @@ func (c *EEBus) writeCurrentLimitData(currents []float64) error {
// set overload protection limits and self consumption limits to identical values
// so if the EV supports self consumption it will be used automatically
return c.cc.WriteCurrentLimitData(currents, currents, &data.EVData)
return c.emobility.EVWriteLoadControlLimits(currents, currents)
}
// MaxCurrent implements the api.Charger interface
@ -514,53 +315,46 @@ var _ api.ChargerEx = (*EEBus)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (c *EEBus) MaxCurrentMillis(current float64) error {
data, err := c.cc.GetData()
chargeState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return err
}
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged {
if chargeState == emobility.EVChargeStateTypeUnplugged {
return errors.New("can't set new current as ev is unplugged")
}
// if data.EVData.Limits[1].Min == 0 {
// c.log.TRACE.Println("!! we did not yet receive min and max currents to validate the call of MaxCurrent, use it as is")
// }
if current < data.EVData.Limits[1].Min {
current = data.EVData.Limits[1].Min
}
if current > data.EVData.Limits[1].Max {
current = data.EVData.Limits[1].Max
}
c.maxCurrent = current
// TODO error handling
currents := []float64{current, current, current}
return c.writeCurrentLimitData(currents)
}
// CurrentPower implements the api.Meter interface
func (c *EEBus) currentPower() (float64, error) {
data, err := c.cc.GetData()
chargeState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return 0, err
}
if chargeState == emobility.EVChargeStateTypeUnplugged {
return 0, nil
}
connectedPhases, err := c.emobility.EVConnectedPhases()
if err != nil {
return 0, err
}
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged {
return 0, nil
powers, err := c.emobility.EVPowerPerPhase()
if err != nil {
return 0, err
}
var power float64
for phase := uint(1); phase <= data.EVData.ConnectedPhases; phase++ {
if phasePower, ok := data.EVData.Measurements.Power.Load(phase); ok {
if _, ok := phasePower.(float64); ok {
power += phasePower.(float64)
}
for index, phasePower := range powers {
if index >= int(connectedPhases) {
break
}
power += phasePower
}
return power, nil
@ -568,41 +362,45 @@ func (c *EEBus) currentPower() (float64, error) {
// ChargedEnergy implements the api.ChargeRater interface
func (c *EEBus) chargedEnergy() (float64, error) {
data, err := c.cc.GetData()
chargeState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return 0, err
}
if chargeState == emobility.EVChargeStateTypeUnplugged {
return 0, nil
}
energy, err := c.emobility.EVChargedEnergy()
if err != nil {
return 0, err
}
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged {
return 0, nil
}
energy := data.EVData.Measurements.ChargedEnergy / 1000
// return kWh
energy /= 1000
return energy, nil
}
// Currents implements the api.MeterCurrent interface
func (c *EEBus) currents() (float64, float64, float64, error) {
data, err := c.cc.GetData()
chargeState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return 0, 0, 0, err
}
if chargeState == emobility.EVChargeStateTypeUnplugged {
return 0, 0, 0, nil
}
currents, err := c.emobility.EVCurrentsPerPhase()
if err != nil {
return 0, 0, 0, err
}
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged {
return 0, 0, 0, nil
}
var currents []float64
for phase := uint(1); phase <= 3; phase++ {
current := 0.0
if value, ok := data.EVData.Measurements.Current.Load(phase); ok {
if _, ok := value.(float64); ok {
current = value.(float64)
}
count := len(currents)
if count < 3 {
for fill := 0; fill < count-3; fill++ {
currents = append(currents, 0)
}
currents = append(currents, current)
}
return currents[0], currents[1], currents[2], nil
@ -612,24 +410,31 @@ var _ api.Identifier = (*EEBus)(nil)
// Identify implements the api.Identifier interface
func (c *EEBus) Identify() (string, error) {
data, err := c.cc.GetData()
if err != nil {
return "", err
}
if !c.connected {
return "", nil
}
if data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnplugged || data.EVData.ChargeState == communication.EVChargeStateEnumTypeUnknown {
chargeState, err := c.emobility.EVCurrentChargeState()
if err != nil {
return "", err
}
if chargeState == emobility.EVChargeStateTypeUnplugged || chargeState == emobility.EVChargeStateTypeUnknown {
return "", nil
}
if len(data.EVData.Identification) > 0 {
return data.EVData.Identification, nil
identification, err := c.emobility.EVIdentification()
if err != nil {
return "", err
}
if identification != "" {
return identification, nil
}
if data.EVData.CommunicationStandard == communication.EVCommunicationStandardEnumTypeIEC61851 {
comStandard, err := c.emobility.EVCommunicationStandard()
if err != nil {
return "", err
}
if comStandard == emobility.EVCommunicationStandardTypeIEC61851 {
return "", nil
}
@ -644,16 +449,21 @@ var _ api.Battery = (*EEBus)(nil)
// SoC implements the api.Vehicle interface
func (c *EEBus) SoC() (float64, error) {
data, err := c.cc.GetData()
socSupported, err := c.emobility.EVSoCSupported()
if err != nil {
return 0, api.ErrMustRetry
}
if !data.EVData.UCSoCAvailable || !data.EVData.SoCDataAvailable {
return 0, api.ErrNotAvailable
}
return data.EVData.Measurements.SoC, nil
if !socSupported {
return 0, api.ErrNotAvailable
}
soc, err := c.emobility.EVSoC()
if err != nil {
return 0, api.ErrNotAvailable
}
return soc, nil
}
var _ loadpoint.Controller = (*EEBus)(nil)
@ -662,11 +472,5 @@ var _ loadpoint.Controller = (*EEBus)(nil)
func (c *EEBus) LoadpointControl(lp loadpoint.API) {
c.lp = lp
// set current known min, max current limits
data, err := c.cc.GetData()
if err != nil {
return
}
c.setLoadpointMinMaxLimits(data)
c.showCurrentChargingSetup()
c.setLoadpointMinMaxLimits()
}

View file

@ -3,7 +3,7 @@ package charger
import (
"testing"
"github.com/evcc-io/eebus/communication"
"github.com/enbility/cemd/emobility"
)
type limitStruct struct {
@ -21,6 +21,67 @@ type testMeasurementStruct struct {
data []measurementStruct
}
// Emobility mock
type EmobilityMock struct {
connectedPhases uint
currents, limitsMin, limitsMax, limitsDefault []float64
}
func (e *EmobilityMock) EVCurrentChargeState() (emobility.EVChargeStateType, error) {
return emobility.EVChargeStateTypeUnknown, nil
}
func (e *EmobilityMock) EVConnectedPhases() (uint, error) {
return e.connectedPhases, nil
}
func (e *EmobilityMock) EVChargedEnergy() (float64, error) {
return 0, nil
}
func (e *EmobilityMock) EVPowerPerPhase() ([]float64, error) {
return []float64{}, nil
}
func (e *EmobilityMock) EVCurrentsPerPhase() ([]float64, error) {
return e.currents, nil
}
func (e *EmobilityMock) EVCurrentLimits() ([]float64, []float64, []float64, error) {
return e.limitsMin, e.limitsMax, e.limitsDefault, nil
}
func (e *EmobilityMock) EVWriteLoadControlLimits(obligations, recommendations []float64) error {
return nil
}
func (e *EmobilityMock) EVCommunicationStandard() (emobility.EVCommunicationStandardType, error) {
return emobility.EVCommunicationStandardTypeUnknown, nil
}
func (e *EmobilityMock) EVIdentification() (string, error) {
return "", nil
}
func (e *EmobilityMock) EVOptimizationOfSelfConsumptionSupported() (bool, error) {
return false, nil
}
func (e *EmobilityMock) EVSoCSupported() (bool, error) {
return false, nil
}
func (e *EmobilityMock) EVSoC() (float64, error) {
return 0, nil
}
func (e *EmobilityMock) EVCoordinatedChargingSupported() (bool, error) {
return false, nil
}
var _ emobility.EmobilityI = (*EmobilityMock)(nil)
func TestEEBusIsCharging(t *testing.T) {
tests := []struct {
name string
@ -30,25 +91,25 @@ func TestEEBusIsCharging(t *testing.T) {
{
"3 phase IEC",
[]limitStruct{
{1, 6.0, 16.0, 0.0},
{2, 6.0, 16.0, 0.0},
{3, 6.0, 16.0, 0.0},
{1, 6, 16, 0},
{2, 6, 16, 0},
{3, 6, 16, 0},
},
[]testMeasurementStruct{
{
false,
[]measurementStruct{
{1, 0.0},
{2, 3.0},
{3, 0.0},
{1, 0},
{2, 3},
{3, 0},
},
},
{
true,
[]measurementStruct{
{1, 6.0},
{2, 0.0},
{3, 1.0},
{1, 6},
{2, 0},
{3, 1},
},
},
},
@ -56,19 +117,19 @@ func TestEEBusIsCharging(t *testing.T) {
{
"1 phase IEC",
[]limitStruct{
{1, 6.0, 16.0, 0.0},
{1, 6, 16, 0},
},
[]testMeasurementStruct{
{
false,
[]measurementStruct{
{1, 2.0},
{1, 2},
},
},
{
true,
[]measurementStruct{
{1, 6.0},
{1, 6},
},
},
},
@ -76,57 +137,57 @@ func TestEEBusIsCharging(t *testing.T) {
{
"3 phase ISO",
[]limitStruct{
{1, 2.2, 16.0, 0.1},
{2, 2.2, 16.0, 0.1},
{3, 2.2, 16.0, 0.1},
{1, 2.2, 16, 0.1},
{2, 2.2, 16, 0.1},
{3, 2.2, 16, 0.1},
},
[]testMeasurementStruct{
{
false,
[]measurementStruct{
{1, 1.0},
{2, 0.0},
{3, 0.0},
{1, 1},
{2, 0},
{3, 0},
},
},
{
true,
[]measurementStruct{
{1, 1.8},
{2, 1.0},
{3, 3.0},
{2, 1},
{3, 3},
},
},
},
},
}
eebus := &EEBus{}
emobilityMock := &EmobilityMock{}
eebus := &EEBus{
emobility: emobilityMock,
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
data := &communication.EVSEClientDataType{
EVData: communication.EVDataType{
ConnectedPhases: 3,
Limits: make(map[uint]communication.EVCurrentLimitType),
Measurements: communication.EVMeasurementsType{},
},
}
emobilityMock.connectedPhases = 3
emobilityMock.limitsMin = make([]float64, 0)
emobilityMock.limitsMax = make([]float64, 0)
emobilityMock.limitsDefault = make([]float64, 0)
for _, limit := range tc.limits {
data.EVData.Limits[limit.phase] = communication.EVCurrentLimitType{
Min: limit.min,
Max: limit.max,
Default: limit.pause,
}
emobilityMock.limitsMin = append(emobilityMock.limitsMin, limit.min)
emobilityMock.limitsMax = append(emobilityMock.limitsMax, limit.max)
emobilityMock.limitsDefault = append(emobilityMock.limitsDefault, limit.pause)
}
for index, m := range tc.measurements {
emobilityMock.currents = make([]float64, 0)
for _, d := range m.data {
data.EVData.Measurements.Current.Store(d.phase, d.current)
emobilityMock.currents = append(emobilityMock.currents, d.current)
}
result := eebus.isCharging(data)
result := eebus.isCharging()
if result != m.expected {
t.Errorf("Failure: test %s, series %d, expected %v, got %v", tc.name, index, m.expected, result)
}

View file

@ -1,10 +1,8 @@
package configure
import (
"crypto/x509/pkix"
"fmt"
certhelper "github.com/evcc-io/eebus/cert"
"github.com/evcc-io/evcc/cmd/shutdown"
"github.com/evcc-io/evcc/server"
)
@ -22,22 +20,14 @@ func (c *CmdConfigure) configureEEBus(conf map[string]interface{}) error {
// eebusCertificate creates EEBUS certificate and returns private/public key
func (c *CmdConfigure) eebusCertificate() (map[string]interface{}, error) {
details := server.EEBUSDetails
subject := pkix.Name{
CommonName: details.DeviceCode,
Country: []string{"DE"},
Organization: []string{details.BrandName},
}
var eebusConfig map[string]interface{}
cert, err := certhelper.CreateCertificate(true, subject)
cert, err := server.CreateEEBUSCertificate()
if err != nil {
return eebusConfig, fmt.Errorf("%s", c.localizedString("Error_EEBUS_Certificate_Create", nil))
}
pubKey, privKey, err := certhelper.GetX509KeyPair(cert)
pubKey, privKey, err := server.GetX509KeyPair(cert)
if err != nil {
return eebusConfig, fmt.Errorf("%s", c.localizedString("Error_EEBUS_Certificate_Use", nil))
}

View file

@ -1,12 +1,10 @@
package cmd
import (
"crypto/x509/pkix"
"os"
"text/template"
"github.com/Masterminds/sprig/v3"
certhelper "github.com/evcc-io/eebus/cert"
"github.com/evcc-io/evcc/server"
"github.com/spf13/cobra"
)
@ -34,22 +32,14 @@ eebus:
`
func generateEEBUSCert() {
details := server.EEBUSDetails
subject := pkix.Name{
CommonName: details.DeviceCode,
Country: []string{"DE"},
Organization: []string{details.BrandName},
cert, err := server.CreateEEBUSCertificate()
if err != nil {
log.FATAL.Fatal("could not create certificate", err)
}
cert, err := certhelper.CreateCertificate(true, subject)
pubKey, privKey, err := server.GetX509KeyPair(cert)
if err != nil {
log.FATAL.Fatal("could not create certificate")
}
pubKey, privKey, err := certhelper.GetX509KeyPair(cert)
if err != nil {
log.FATAL.Fatal("could not process generated certificate")
log.FATAL.Fatal("could not process generated certificate", err)
}
t := template.Must(template.New("out").Funcs(template.FuncMap(sprig.FuncMap())).Parse(tmpl))

11
go.mod
View file

@ -27,7 +27,8 @@ require (
github.com/dylanmei/iso8601 v0.1.0
github.com/eclipse/paho.mqtt.golang v1.4.2
github.com/emirpasic/gods v1.18.1
github.com/evcc-io/eebus v0.0.0-20221023111026-e3c9e4d1f3c8
github.com/enbility/cemd v0.1.3
github.com/enbility/eebus-go v0.1.3
github.com/fatih/structs v1.1.0
github.com/foogod/go-powerwall v0.2.0
github.com/glebarez/sqlite v1.5.0
@ -87,7 +88,7 @@ require (
github.com/writeas/go-strip-markdown v2.0.1+incompatible
gitlab.com/bboehmke/sunny v0.15.1-0.20211022160056-2fba1c86ade6
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
golang.org/x/net v0.3.0
golang.org/x/net v0.4.0
golang.org/x/oauth2 v0.3.0
golang.org/x/sync v0.1.0
golang.org/x/text v0.5.0
@ -98,6 +99,8 @@ require (
gorm.io/gorm v1.24.2
)
require github.com/ahmetb/go-linq/v3 v3.2.0 // indirect
require (
cloud.google.com/go/compute v1.14.0 // indirect
cloud.google.com/go/compute/metadata v0.2.2 // indirect
@ -129,6 +132,7 @@ require (
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holoplot/go-avahi v1.0.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
@ -171,12 +175,13 @@ require (
github.com/thoas/go-funk v0.9.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/tools v0.3.0 // indirect
golang.org/x/tools v0.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221205194025-8222ab48f5fc // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect

21
go.sum
View file

@ -88,6 +88,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWso
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ahmetb/go-linq/v3 v3.2.0 h1:BEuMfp+b59io8g5wYzNoFe9pWPalRklhlhbiU3hYZDE=
github.com/ahmetb/go-linq/v3 v3.2.0/go.mod h1:haQ3JfOeWK8HpVxMtHHEMPVgBKiYyQ+f1/kLZh/cj9U=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -222,6 +224,10 @@ github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/enbility/cemd v0.1.3 h1:FkmbuS7ngKBsQNtapEjtorhkjJM9ay8uc9VZta7MlDA=
github.com/enbility/cemd v0.1.3/go.mod h1:dDJWVmkT2bysTPBQ+YYpeu8gLMic0zEzlVTYNL7u+S8=
github.com/enbility/eebus-go v0.1.3 h1:YYDkd5h9jgT126B1OG/lvawYcxweqKMXP0DvfC5cj7w=
github.com/enbility/eebus-go v0.1.3/go.mod h1:BaXy+yk3pAURDqjaaPkxqJ6nlPDcEOT7ARs85U9T6YI=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -233,8 +239,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evcc-io/eebus v0.0.0-20221023111026-e3c9e4d1f3c8 h1:0qWgzAMPCjGmSVucu/DJkPWk5KPnrD7O114dnSBi+uY=
github.com/evcc-io/eebus v0.0.0-20221023111026-e3c9e4d1f3c8/go.mod h1:kkKB+GC+F1XOFdy68BCwRbLPuFUqAZP3UIk4Vhr/FYA=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@ -314,6 +318,7 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
@ -486,6 +491,8 @@ github.com/hasura/go-graphql-client v0.8.1 h1:yU4888urgkW4L47cs+QQDXl3YfVaNraUqy
github.com/hasura/go-graphql-client v0.8.1/go.mod h1:NVifIwv+YFIUYGLQ7SM2/vBbzS/9rFP4vmIf/vf/zXM=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/holoplot/go-avahi v1.0.1 h1:XcqR2keL4qWRnlxHD5CAOdWpLFZJ+EOUK0vEuylfvvk=
github.com/holoplot/go-avahi v1.0.1/go.mod h1:qH5psEKb0DK+BRplMfc+RY4VMOlbf6mqfxgpMy6aP0M=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
@ -890,6 +897,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/bboehmke/sunny v0.15.1-0.20211022160056-2fba1c86ade6 h1:73pM5aQqFkQfmXyGf3+xeWWg98J03xtDIkA5TE37ERU=
gitlab.com/bboehmke/sunny v0.15.1-0.20211022160056-2fba1c86ade6/go.mod h1:F5AIuL7kYteSJFR5E+YEocxIdpyCXmtDciFmMQVjP88=
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
@ -1036,8 +1045,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1267,8 +1276,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,53 +1,47 @@
package server
import (
"context"
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"strconv"
"strings"
"sync"
"github.com/evcc-io/eebus/cert"
"github.com/evcc-io/eebus/communication"
"github.com/evcc-io/eebus/mdns"
"github.com/evcc-io/eebus/server"
"github.com/evcc-io/eebus/ship"
"github.com/evcc-io/eebus/spine/model"
"github.com/enbility/cemd/cem"
"github.com/enbility/cemd/emobility"
"github.com/enbility/eebus-go/service"
"github.com/enbility/eebus-go/spine/model"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/machine"
"github.com/libp2p/zeroconf/v2"
)
var EEBUSDetails = communication.ManufacturerDetails{
BrandName: "EVCC",
DeviceName: "EVCC",
DeviceCode: "EVCC_HEMS_01",
DeviceAddress: "EVCC_HEMS",
}
const (
EEBUSBrandName string = "EVCC"
EEBUSModel string = "HEMS"
EEBUSDeviceCode string = "EVCC_HEMS_01" // used as common name in cert generation
)
type EEBusClientCBs struct {
onConnect func(string, ship.Conn) error
onConnect func(string) // , ship.Conn) error
onDisconnect func(string)
}
type EEBus struct {
mux sync.Mutex
log *util.Logger
srv *server.Server
id string
zc *zeroconf.Server
clients map[string]EEBusClientCBs
ipaddress map[string]string
connectedClients map[string]ship.Conn
discoveredClients map[string]*zeroconf.ServiceEntry
clientInConnection map[string]bool
Cem *cem.CemImpl
browseMDNSRunning bool
mux sync.Mutex
log *util.Logger
SKI string
clients map[string]EEBusClientCBs
}
var EEBusInstance *EEBus
@ -68,25 +62,17 @@ func NewEEBus(other map[string]interface{}) (*EEBus, error) {
return nil, err
}
// if !sponsor.IsAuthorized() {
// return nil, errors.New("eebus requires evcc sponsorship, register at https://cloud.evcc.io")
// }
details := EEBusInstance.DeviceInfo()
log := util.NewLogger("eebus")
if len(cc.ShipID) == 0 {
var err error
protectedID, err := machine.ProtectedID("evcc-eebus")
if err != nil {
return nil, err
}
var err error
protectedID, err := machine.ProtectedID("evcc-eebus")
if err != nil {
return nil, err
}
serial := fmt.Sprintf("%s-%0x", "EVCC", protectedID[:8])
cc.ShipID, err = ship.UniqueIDWithProtectedID(details.BrandName, protectedID)
if err != nil {
return nil, err
}
if len(cc.ShipID) != 0 {
serial = cc.ShipID
}
certificate, err := tls.X509KeyPair(cc.Certificate.Public, cc.Certificate.Private)
@ -94,340 +80,206 @@ func NewEEBus(other map[string]interface{}) (*EEBus, error) {
return nil, err
}
srv := &server.Server{
Log: log.TRACE,
Addr: cc.Uri,
Path: "/ship/",
Certificate: certificate,
ID: cc.ShipID,
Interfaces: cc.Interfaces,
Brand: details.BrandName,
Model: details.DeviceCode,
Type: string(model.DeviceTypeEnumTypeEnergyManagementSystem),
Register: true,
}
zc, err := srv.Announce()
_, portValue, err := net.SplitHostPort(cc.Uri)
if err != nil {
return nil, err
}
ski, err := cert.SkiFromCert(certificate)
port, err := strconv.Atoi(portValue)
if err != nil {
return nil, err
}
// TODO: get the voltage from the site
configuration, err := service.NewConfiguration(
EEBUSBrandName, EEBUSBrandName, EEBUSModel, serial,
model.DeviceTypeTypeEnergyManagementSystem, port, certificate, 230,
)
if err != nil {
return nil, err
}
// for backward compatibility
configuration.SetAlternateMdnsServiceName("EVCC_HEMS_01")
configuration.SetAlternateIdentifier(serial)
configuration.SetInterfaces(cc.Interfaces)
configuration.SetRegisterAutoAccept(true)
ski, err := SkiFromCert(certificate)
if err != nil {
return nil, err
}
c := &EEBus{
zc: zc,
log: log,
srv: srv,
id: cc.ShipID,
clients: make(map[string]EEBusClientCBs),
ipaddress: make(map[string]string),
connectedClients: make(map[string]ship.Conn),
discoveredClients: make(map[string]*zeroconf.ServiceEntry),
clientInConnection: make(map[string]bool),
SKI: ski,
log: log,
clients: make(map[string]EEBusClientCBs),
SKI: ski,
}
c.Cem = cem.NewCEM(configuration, c, c)
if err := c.Cem.Setup(true); err != nil {
return nil, err
}
return c, nil
}
func (c *EEBus) DeviceInfo() communication.ManufacturerDetails {
return EEBUSDetails
}
func (c *EEBus) Register(ski, ip string, shipConnectHandler func(string, ship.Conn) error, shipDisconnectHandler func(string)) {
func (c *EEBus) Register(ski, ip string, connectHandler func(string), disconnectHandler func(string)) *emobility.EMobilityImpl {
ski = strings.ReplaceAll(ski, "-", "")
ski = strings.ReplaceAll(ski, " ", "")
ski = strings.ToLower(ski)
c.log.TRACE.Printf("registering ski: %s", ski)
if ski == c.SKI {
log.FATAL.Fatal("The charger SKI can not be identical to the SKI of evcc!")
c.log.FATAL.Fatal("The charger SKI can not be identical to the SKI of evcc!")
}
c.mux.Lock()
c.clients[ski] = EEBusClientCBs{onConnect: shipConnectHandler, onDisconnect: shipDisconnectHandler}
if ip != "" {
c.ipaddress[ski] = ip
}
c.mux.Unlock()
// maybe the SKI is already discovered
_ = c.handleDiscoveredSKI(ski)
}
func (c *EEBus) Run() {
go c.browseMDNS()
ln := &server.Listener{
Log: c.log.TRACE,
AccessMethod: c.id,
Handler: c.shipHandler,
}
if err := c.srv.Listen(ln, c.certificateHandler); err != nil {
c.log.ERROR.Println("eebus listen:", err)
}
}
func (c *EEBus) browseMDNS() {
c.browseMDNSRunning = true
// Let's start from scratch
c.mux.Lock()
for k := range c.discoveredClients {
delete(c.discoveredClients, k)
}
c.mux.Unlock()
entries := make(chan *zeroconf.ServiceEntry)
defer close(entries)
go c.discoverDNS(entries, func(entry *zeroconf.ServiceEntry) {
c.addDisoveredEntry(entry)
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := zeroconf.Browse(ctx, ship.ZeroconfType, ship.ZeroconfDomain, entries); err != nil {
panic(fmt.Errorf("failed to browse: %w", err))
}
}
func (c *EEBus) discoverDNS(results <-chan *zeroconf.ServiceEntry, connector func(*zeroconf.ServiceEntry)) {
for entry := range results {
c.log.TRACE.Println("mDNS:", entry.HostName, entry.AddrIPv4, entry.Text)
connector(entry)
}
// The mDNS Browse has timed out
c.browseMDNSRunning = false
c.browseMissingClients()
}
func (c *EEBus) Shutdown() {
c.zc.Shutdown()
}
func (c *EEBus) addDisoveredEntry(entry *zeroconf.ServiceEntry) {
// we need to get the SKI only
svc, err := mdns.NewFromDNSEntry(entry)
if err == nil {
if entry.Text == nil {
c.log.TRACE.Printf("Ignoring discovered mDNS entry as it has no TXT record: %s", entry.HostName)
return
}
if svc.SKI == c.SKI {
c.log.TRACE.Println("Ignoring discovered mDNS entry as it is this service itself")
return
}
c.patchMdnsEntryWithProvidedIP(svc.SKI, entry)
if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil {
c.log.TRACE.Printf("Ignoring discovered mDNS entry as it has no IPv4 and IPv6 address: %s", entry.HostName)
return
}
c.mux.Lock()
c.discoveredClients[svc.SKI] = entry
c.mux.Unlock()
// maybe the SKI is already registered
_ = c.handleDiscoveredSKI(svc.SKI)
} else {
c.log.TRACE.Printf("%s: could not create ship service from DNS entry: %v", entry.HostName, err)
}
}
func (c *EEBus) handleDiscoveredSKI(ski string) error {
c.mux.Lock()
_, connected := c.connectedClients[ski]
_, registered := c.clients[ski]
entry, discovered := c.discoveredClients[ski]
_, connecting := c.clientInConnection[ski]
c.mux.Unlock()
c.log.TRACE.Printf("client %s connected %t, registered %t, discovered %t, connecting %t", ski, connected, registered, discovered, connecting)
if !connected && discovered && registered && !connecting {
c.mux.Lock()
c.clientInConnection[ski] = true
c.mux.Unlock()
if err := c.connectDiscoveredEntry(ski, entry); err != nil {
c.mux.Lock()
delete(c.connectedClients, ski)
delete(c.clientInConnection, ski)
c.mux.Unlock()
return err
}
c.mux.Lock()
delete(c.clientInConnection, ski)
c.mux.Unlock()
}
return nil
}
// add the IP address from the charger configuration to the mDNS entry if it is missing
func (c *EEBus) patchMdnsEntryWithProvidedIP(ski string, entry *zeroconf.ServiceEntry) {
address, exists := c.ipaddress[ski]
if entry.AddrIPv4 == nil && exists && address != "" {
if ip := net.ParseIP(address); ip != nil {
entry.AddrIPv4 = []net.IP{ip}
}
}
}
func (c *EEBus) connectDiscoveredEntry(ski string, entry *zeroconf.ServiceEntry) error {
c.patchMdnsEntryWithProvidedIP(ski, entry)
svc, err := mdns.NewFromDNSEntry(entry)
var conn ship.Conn
if err == nil {
c.log.TRACE.Printf("%s: client connect", entry.HostName)
conn, err = svc.Connect(c.log.TRACE, c.id, c.srv.Certificate, c.shipCloseHandler)
}
if err != nil {
c.log.TRACE.Printf("%s: client done: %v", entry.HostName, err)
return err
}
err = c.shipHandler(svc.SKI, conn)
if err != nil {
log.FATAL.Fatalf("%s: error calling shipHandler: %v", entry.HostName, err)
return err
}
return nil
}
func (c *EEBus) certificateHandler(leaf *x509.Certificate) error {
ski, err := cert.SkiFromX509(leaf)
if err != nil {
return err
}
c.log.TRACE.Printf("verifying client ski: %s", ski)
serviceDetails := service.NewServiceDetails(ski)
serviceDetails.SetIPv4(ip)
c.mux.Lock()
defer c.mux.Unlock()
c.clients[ski] = EEBusClientCBs{onConnect: connectHandler, onDisconnect: disconnectHandler}
for client := range c.clients {
if client == ski {
c.log.TRACE.Printf("client ski found")
return nil
}
}
c.log.TRACE.Printf("client ski not found!")
return fmt.Errorf("client ski not allowed: %s", ski)
return c.Cem.RegisterEmobilityRemoteDevice(serviceDetails)
}
func (c *EEBus) shipHandler(ski string, conn ship.Conn) error {
for client, cb := range c.clients {
if client == ski {
c.mux.Lock()
currentConnection, found := c.connectedClients[ski]
c.mux.Unlock()
connect := true
c.log.TRACE.Printf("client %s found? %t", ski, found)
if found {
if currentConnection.IsConnectionClosed() {
c.log.TRACE.Printf("client has closed connection")
} else {
c.log.TRACE.Printf("client has no closed connection")
connect = false
}
}
c.log.TRACE.Printf("client %s connect? %t", ski, connect)
if connect {
c.mux.Lock()
c.connectedClients[ski] = conn
c.mux.Unlock()
return cb.onConnect(ski, conn)
}
}
}
return errors.New("client not registered")
func (c *EEBus) Run() {
c.Cem.Start()
}
// handles connection closed
func (c *EEBus) shipCloseHandler(ski string) {
func (c *EEBus) Shutdown() {
c.Cem.Shutdown()
}
// 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) {
c.mux.Lock()
if conn, ok := c.connectedClients[ski]; ok {
for clientSki, client := range c.clients {
if clientSki != ski {
continue
}
defer c.mux.Unlock()
if conn.IsConnectionClosed() {
c.log.TRACE.Printf("close client %s connection", ski)
client.onDisconnect(ski)
}
// always remove client on close
delete(c.connectedClients, ski)
break
}
}
c.mux.Unlock()
c.browseMissingClients()
}
// search for registered but not connected clients right away
// this fixes registered clients closing the connection whysoever
func (c *EEBus) browseMissingClients() {
if c.browseMDNSRunning {
client, exists := c.clients[ski]
if !exists {
return
}
client.onConnect(ski)
}
if len(c.clients) == len(c.connectedClients) {
func (c *EEBus) RemoteSKIDisconnected(service *service.EEBUSService, ski string) {
c.mux.Lock()
defer c.mux.Unlock()
client, exists := c.clients[ski]
if !exists {
return
}
client.onDisconnect(ski)
}
c.browseMDNSRunning = true
func (h *EEBus) ReportServiceShipID(ski string, shipdID string) {}
// first try the discovered client, if that not works, browse again
foundMissingClients := len(c.clients) - len(c.connectedClients)
var failedClientSKIs []string
for ski, entry := range c.discoveredClients {
if _, ok := c.clients[ski]; !ok {
continue
// EEBUS Logging interface
func (c *EEBus) Trace(args ...interface{}) {
c.log.TRACE.Println(args...)
}
func (c *EEBus) Tracef(format string, args ...interface{}) {
c.log.TRACE.Printf(format, args...)
}
func (c *EEBus) Debug(args ...interface{}) {
c.log.DEBUG.Println(args...)
}
func (c *EEBus) Debugf(format string, args ...interface{}) {
c.log.DEBUG.Printf(format, args...)
}
func (c *EEBus) Info(args ...interface{}) {
c.log.INFO.Println(args...)
}
func (c *EEBus) Infof(format string, args ...interface{}) {
c.log.INFO.Printf(format, args...)
}
func (c *EEBus) Error(args ...interface{}) {
c.log.ERROR.Println(args...)
}
func (c *EEBus) Errorf(format string, args ...interface{}) {
c.log.ERROR.Printf(format, args...)
}
// Certificate helpers
// CreateEEBUSCertificate returns a newly created EEBUS compatible certificate
func CreateEEBUSCertificate() (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)
}
if _, ok := c.connectedClients[ski]; !ok {
c.log.TRACE.Printf("%s: client not connected, trying to connect\n", entry.HostName)
if err := c.handleDiscoveredSKI(ski); err != nil {
failedClientSKIs = append(failedClientSKIs, ski)
continue
}
foundMissingClients--
}
}
c.mux.Lock()
for _, ski := range failedClientSKIs {
delete(c.discoveredClients, ski)
}
c.mux.Unlock()
c.browseMDNSRunning = false
if foundMissingClients > 0 {
c.browseMDNS()
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 := &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)
}