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:
parent
e3dc7986a0
commit
ae393838f4
7 changed files with 452 additions and 741 deletions
468
charger/eebus.go
468
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
20
cmd/eebus.go
20
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))
|
||||
|
|
|
|||
11
go.mod
11
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
|
||||
|
|
|
|||
21
go.sum
21
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=
|
||||
|
|
|
|||
526
server/eebus.go
526
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue