diff --git a/charger/eebus.go b/charger/eebus.go index fae4df73b..ad9f382ad 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -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() } diff --git a/charger/eebus_test.go b/charger/eebus_test.go index f087f1223..b0ff70c42 100644 --- a/charger/eebus_test.go +++ b/charger/eebus_test.go @@ -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) } diff --git a/cmd/configure/eebus.go b/cmd/configure/eebus.go index 5e3cf3b38..06ea52323 100644 --- a/cmd/configure/eebus.go +++ b/cmd/configure/eebus.go @@ -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)) } diff --git a/cmd/eebus.go b/cmd/eebus.go index ee8318723..062050779 100644 --- a/cmd/eebus.go +++ b/cmd/eebus.go @@ -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)) diff --git a/go.mod b/go.mod index e21bf8bbf..f903627ef 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b1a21517c..8655aaa6f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/server/eebus.go b/server/eebus.go index 65fad88ac..9c29f84d4 100644 --- a/server/eebus.go +++ b/server/eebus.go @@ -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) +}