diff --git a/charger/keba-modbus.go b/charger/keba-modbus.go new file mode 100644 index 000000000..67cfe3382 --- /dev/null +++ b/charger/keba-modbus.go @@ -0,0 +1,276 @@ +package charger + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + "github.com/evcc-io/evcc/util/sponsor" +) + +// https://www.keba.com/en/emobility/service-support/downloads/Downloads +// https://www.keba.com/download/x/dea7ae6b84/kecontactp30modbustcp_pgen.pdf + +// Keba is an api.Charger implementation +type Keba struct { + conn *modbus.Connection +} + +const ( + kebaRegChargingState = 1000 + kebaRegCableState = 1004 + kebaRegCurrents = 1008 // 6 regs, mA + kebaRegSerial = 1014 // leading zeros trimmed + kebaRegProduct = 1016 + kebaRegFirmware = 1018 + kebaRegPower = 1020 // mW + kebaRegEnergy = 1036 // Wh + kebaRegVoltages = 1040 // 6 regs, V + kebaRegRfid = 1500 // hex + kebaRegSessionEnergy = 1502 // Wh + kebaRegPhaseSource = 1550 + kebaRegPhaseState = 1552 + kebaRegFailsafeTimeout = 1602 + kebaRegMaxCurrent = 5004 // mA + kebaRegEnable = 5014 + kebaRegTriggerPhase = 5052 +) + +func init() { + registry.Add("keba-modbus", NewKebaFromConfig) +} + +// go:generate go run ../cmd/tools/decorate.go -f decorateKeba -b *Keba -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.Identifier,Identify,func() (string, error)" -t "api.PhaseSwitcher,Phases1p3p,func(int) error" + +// NewKebaFromConfig creates a new Keba ModbusTCP charger +func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := modbus.TcpSettings{ + ID: 255, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + wb, err := NewKeba(cc.URI, cc.ID) + if err != nil { + return nil, err + } + + // features + var ( + currentPower, totalEnergy func() (float64, error) + currents func() (float64, float64, float64, error) + identify func() (string, error) + ) + + b, err := wb.conn.ReadHoldingRegisters(kebaRegProduct, 2) + if err != nil { + return nil, err + } + + if features := binary.BigEndian.Uint32(b); (features/100)%10 > 0 { + currentPower = wb.currentPower + totalEnergy = wb.totalEnergy + currents = wb.currents + } + + if features := binary.BigEndian.Uint32(b); features%10 > 0 { + identify = wb.identify + } + + // phases + b, err = wb.conn.ReadHoldingRegisters(kebaRegPhaseSource, 2) + if err != nil { + return nil, err + } + + var phases func(int) error + if source := binary.BigEndian.Uint32(b); source == 3 { + phases = wb.phases1p3p + } + + return decorateKeba(wb, currentPower, totalEnergy, currents, identify, phases), nil +} + +// NewKeba creates a new charger +func NewKeba(uri string, slaveID uint8) (*Keba, error) { + conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID) + if err != nil { + return nil, err + } + + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + log := util.NewLogger("keba") + conn.Logger(log.TRACE) + + // per Keba docs + // conn.Delay(500 * time.Millisecond) + + wb := &Keba{ + conn: conn, + } + + return wb, err +} + +// Status implements the api.Charger interface +func (wb *Keba) Status() (api.ChargeStatus, error) { + b, err := wb.conn.ReadHoldingRegisters(kebaRegCableState, 2) + if err != nil { + return api.StatusNone, err + } + + fmt.Printf("1004: %0x\n", b) + + switch status := binary.BigEndian.Uint32(b); status { + case 0: + return api.StatusA, nil + + case 1, 3, 5: + return api.StatusB, nil + + case 7: + b, err := wb.conn.ReadHoldingRegisters(kebaRegChargingState, 2) + if err != nil { + return api.StatusNone, err + } + fmt.Printf("1000: %0x\n", b) + if binary.BigEndian.Uint32(b) == 3 { + return api.StatusC, err + } + return api.StatusB, nil + + default: + return api.StatusNone, fmt.Errorf("invalid status: %d", status) + } +} + +// Enabled implements the api.Charger interface +func (wb *Keba) Enabled() (bool, error) { + b, err := wb.conn.ReadHoldingRegisters(kebaRegChargingState, 2) + if err != nil { + return false, err + } + + fmt.Printf("1000: %0x\n", b) + + return binary.BigEndian.Uint32(b) != 5, nil +} + +// Enable implements the api.Charger interface +func (wb *Keba) Enable(enable bool) error { + var u uint16 + if enable { + u = 1 + } + _, err := wb.conn.WriteSingleRegister(kebaRegEnable, u) + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *Keba) MaxCurrent(current int64) error { + return wb.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*Keba)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (wb *Keba) MaxCurrentMillis(current float64) error { + u := uint16(current * 1000) + _, err := wb.conn.WriteSingleRegister(kebaRegMaxCurrent, u) + return err +} + +// currentPower implements the api.Meter interface +func (wb *Keba) currentPower() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(kebaRegPower, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)) / 1e3, nil +} + +// totalEnergy implements the api.MeterEnergy interface +func (wb *Keba) totalEnergy() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(kebaRegEnergy, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)) / 1e3, nil +} + +// currents implements the api.PhaseCurrents interface +func (wb *Keba) currents() (float64, float64, float64, error) { + var res [3]float64 + for i := uint16(0); i < 3; i++ { + b, err := wb.conn.ReadHoldingRegisters(kebaRegCurrents+2*i, 2) + if err != nil { + return 0, 0, 0, err + } + + res[i] = float64(binary.BigEndian.Uint32(b)) / 1e3 + } + + return res[0], res[1], res[2], nil +} + +// identify implements the api.Identifier interface +func (wb *Keba) identify() (string, error) { + b, err := wb.conn.ReadHoldingRegisters(kebaRegRfid, 2) + if err != nil { + return "", err + } + + id := hex.EncodeToString(b) + if id == "00000000" { + id = "" + } + + return id, nil +} + +// phases1p3p implements the api.PhaseSwitcher interface +func (wb *Keba) phases1p3p(phases int) error { + var u uint16 + if phases == 3 { + u = 1 + } + + _, err := wb.conn.WriteSingleRegister(kebaRegTriggerPhase, u) + return err +} + +var _ api.Diagnosis = (*Keba)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *Keba) Diagnose() { + if b, err := wb.conn.ReadHoldingRegisters(kebaRegSerial, 2); err == nil { + fmt.Printf("\tSerial:\t%s\n", strings.TrimLeft(strconv.Itoa(int(binary.BigEndian.Uint32(b))), "0")) + } + if b, err := wb.conn.ReadHoldingRegisters(kebaRegFirmware, 2); err == nil { + fmt.Printf("\tFirmware:\t%d.%d.%d\n", b[0], b[1], b[2]) + } + if b, err := wb.conn.ReadHoldingRegisters(kebaRegProduct, 2); err == nil { + fmt.Printf("\tProduct:\t%6d\n", binary.BigEndian.Uint32(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(kebaRegPhaseSource, 2); err == nil { + fmt.Printf("\tPhases source:\t%d\n", binary.BigEndian.Uint32(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(kebaRegPhaseState, 2); err == nil { + fmt.Printf("\tPhases state:\t%d\n", binary.BigEndian.Uint32(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(kebaRegFailsafeTimeout, 2); err == nil { + fmt.Printf("\tFailsafe timeout:\t%ds\n", binary.BigEndian.Uint32(b)) + } +} diff --git a/charger/keba-modbus_decorators.go b/charger/keba-modbus_decorators.go new file mode 100644 index 000000000..6344393bf --- /dev/null +++ b/charger/keba-modbus_decorators.go @@ -0,0 +1,593 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateKeba(base *Keba, meter func() (float64, error), meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error), identifier func() (string, error), phaseSwitcher func(int) error) api.Charger { + switch { + case identifier == nil && meter == nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher == nil: + return base + + case identifier == nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Meter + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + } + + case identifier == nil && meter == nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.MeterEnergy + }{ + Keba: base, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case identifier == nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Meter + api.MeterEnergy + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case identifier == nil && meter == nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.PhaseCurrents + }{ + Keba: base, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier == nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Meter + api.PhaseCurrents + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier == nil && meter == nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.MeterEnergy + api.PhaseCurrents + }{ + Keba: base, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier == nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Meter + api.MeterEnergy + api.PhaseCurrents + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier != nil && meter == nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + } + + case identifier != nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.Meter + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + } + + case identifier != nil && meter == nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.MeterEnergy + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case identifier != nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.MeterEnergy + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case identifier != nil && meter == nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.PhaseCurrents + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier != nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.PhaseCurrents + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier != nil && meter == nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.MeterEnergy + api.PhaseCurrents + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier != nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher == nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.MeterEnergy + api.PhaseCurrents + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + } + + case identifier == nil && meter == nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.PhaseSwitcher + }{ + Keba: base, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Meter + api.PhaseSwitcher + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter == nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.MeterEnergy + api.PhaseSwitcher + }{ + Keba: base, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Meter + api.MeterEnergy + api.PhaseSwitcher + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter == nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Meter + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter == nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.MeterEnergy + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier == nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Meter + api.MeterEnergy + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter == nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter != nil && meterEnergy == nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter == nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.MeterEnergy + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter != nil && meterEnergy != nil && phaseCurrents == nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.MeterEnergy + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter == nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter != nil && meterEnergy == nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter == nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.MeterEnergy + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case identifier != nil && meter != nil && meterEnergy != nil && phaseCurrents != nil && phaseSwitcher != nil: + return &struct { + *Keba + api.Identifier + api.Meter + api.MeterEnergy + api.PhaseCurrents + api.PhaseSwitcher + }{ + Keba: base, + Identifier: &decorateKebaIdentifierImpl{ + identifier: identifier, + }, + Meter: &decorateKebaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateKebaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + phaseCurrents: phaseCurrents, + }, + PhaseSwitcher: &decorateKebaPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decorateKebaIdentifierImpl struct { + identifier func() (string, error) +} + +func (impl *decorateKebaIdentifierImpl) Identify() (string, error) { + return impl.identifier() +} + +type decorateKebaMeterImpl struct { + meter func() (float64, error) +} + +func (impl *decorateKebaMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decorateKebaMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateKebaMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} + +type decorateKebaPhaseCurrentsImpl struct { + phaseCurrents func() (float64, float64, float64, error) +} + +func (impl *decorateKebaPhaseCurrentsImpl) Currents() (float64, float64, float64, error) { + return impl.phaseCurrents() +} + +type decorateKebaPhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateKebaPhaseSwitcherImpl) Phases1p3p(phases int) error { + return impl.phaseSwitcher(phases) +} diff --git a/charger/keba.go b/charger/keba-udp.go similarity index 75% rename from charger/keba.go rename to charger/keba-udp.go index b14a1ed47..4c0c256f2 100644 --- a/charger/keba.go +++ b/charger/keba-udp.go @@ -18,34 +18,29 @@ const ( udpTimeout = time.Second ) -// RFID contains access credentials -type RFID struct { - Tag string -} - -// Keba is an api.Charger implementation with configurable getters and setters. -type Keba struct { +// KebaUdp is an api.Charger implementation +type KebaUdp struct { log *util.Logger conn string - rfid RFID + rfid keba.RFID timeout time.Duration recv chan keba.UDPMsg sender *keba.Sender } func init() { - registry.Add("keba", NewKebaFromConfig) + registry.Add("keba-udp", NewKebaUdpFromConfig) } -//go:generate go run ../cmd/tools/decorate.go -f decorateKeba -b *Keba -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" +//go:generate go run ../cmd/tools/decorate.go -f decorateKebaUdp -b *KebaUdp -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -// NewKebaFromConfig creates a new configurable charger -func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) { +// NewKebaUdpFromConfig creates a new Keba UDP charger +func NewKebaUdpFromConfig(other map[string]interface{}) (api.Charger, error) { cc := struct { URI string Serial string Timeout time.Duration - RFID RFID + RFID keba.RFID }{ Timeout: udpTimeout, } @@ -54,7 +49,7 @@ func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) { return nil, err } - k, err := NewKeba(cc.URI, cc.Serial, cc.RFID, cc.Timeout) + k, err := NewKebaUdp(cc.URI, cc.Serial, cc.RFID, cc.Timeout) if err != nil { return nil, err } @@ -65,14 +60,14 @@ func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) { } if energy > 0 { - return decorateKeba(k, k.currentPower, k.totalEnergy, k.currents), nil + return decorateKebaUdp(k, k.currentPower, k.totalEnergy, k.currents), nil } return k, err } -// NewKeba creates a new charger -func NewKeba(uri, serial string, rfid RFID, timeout time.Duration) (*Keba, error) { +// NewKebaUdp creates a new charger +func NewKebaUdp(uri, serial string, rfid keba.RFID, timeout time.Duration) (*KebaUdp, error) { log := util.NewLogger("keba") instance, err := keba.Instance(log) @@ -84,7 +79,7 @@ func NewKeba(uri, serial string, rfid RFID, timeout time.Duration) (*Keba, error conn := util.DefaultPort(uri, keba.Port) sender, err := keba.NewSender(log, conn) - c := &Keba{ + c := &KebaUdp{ log: log, conn: conn, rfid: rfid, @@ -103,7 +98,7 @@ func NewKeba(uri, serial string, rfid RFID, timeout time.Duration) (*Keba, error return c, err } -func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, closeC <-chan struct{}) { +func (c *KebaUdp) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, closeC <-chan struct{}) { t := time.NewTimer(c.timeout) defer close(resC) defer close(errC) @@ -129,7 +124,7 @@ func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, c } } -func (c *Keba) roundtrip(msg string, report int, res interface{}) error { +func (c *KebaUdp) roundtrip(msg string, report int, res interface{}) error { resC := make(chan keba.UDPMsg) errC := make(chan error) closeC := make(chan struct{}) @@ -172,7 +167,7 @@ func (c *Keba) roundtrip(msg string, report int, res interface{}) error { } // Status implements the api.Charger interface -func (c *Keba) Status() (api.ChargeStatus, error) { +func (c *KebaUdp) Status() (api.ChargeStatus, error) { var kr keba.Report2 err := c.roundtrip("report", 2, &kr) if err != nil { @@ -193,7 +188,7 @@ func (c *Keba) Status() (api.ChargeStatus, error) { } // Enabled implements the api.Charger interface -func (c *Keba) Enabled() (bool, error) { +func (c *KebaUdp) Enabled() (bool, error) { var kr keba.Report2 err := c.roundtrip("report", 2, &kr) if err != nil { @@ -204,7 +199,7 @@ func (c *Keba) Enabled() (bool, error) { } // enableRFID sends RFID credentials to enable charge -func (c *Keba) enableRFID() error { +func (c *KebaUdp) enableRFID() error { // check if authorization required var kr keba.Report2 if err := c.roundtrip("report", 2, &kr); err != nil { @@ -227,7 +222,7 @@ func (c *Keba) enableRFID() error { } // Enable implements the api.Charger interface -func (c *Keba) Enable(enable bool) error { +func (c *KebaUdp) Enable(enable bool) error { if enable { if err := c.enableRFID(); err != nil { return err @@ -249,7 +244,7 @@ func (c *Keba) Enable(enable bool) error { } // MaxCurrent implements the api.Charger interface -func (c *Keba) MaxCurrent(current int64) error { +func (c *KebaUdp) MaxCurrent(current int64) error { d := 1000 * current var resp string @@ -260,10 +255,10 @@ func (c *Keba) MaxCurrent(current int64) error { return nil } -var _ api.ChargerEx = (*Keba)(nil) +var _ api.ChargerEx = (*KebaUdp)(nil) // MaxCurrentMillis implements the api.ChargerEx interface -func (c *Keba) MaxCurrentMillis(current float64) error { +func (c *KebaUdp) MaxCurrentMillis(current float64) error { d := int(1000 * current) var resp string @@ -278,7 +273,7 @@ func (c *Keba) MaxCurrentMillis(current float64) error { } // currentPower implements the api.Meter interface -func (c *Keba) currentPower() (float64, error) { +func (c *KebaUdp) currentPower() (float64, error) { var kr keba.Report3 err := c.roundtrip("report", 3, &kr) @@ -287,7 +282,7 @@ func (c *Keba) currentPower() (float64, error) { } // totalEnergy implements the api.MeterEnergy interface -func (c *Keba) totalEnergy() (float64, error) { +func (c *KebaUdp) totalEnergy() (float64, error) { var kr keba.Report3 err := c.roundtrip("report", 3, &kr) @@ -296,7 +291,7 @@ func (c *Keba) totalEnergy() (float64, error) { } // currents implements the api.PhaseCurrents interface -func (c *Keba) currents() (float64, float64, float64, error) { +func (c *KebaUdp) currents() (float64, float64, float64, error) { var kr keba.Report3 err := c.roundtrip("report", 3, &kr) @@ -304,19 +299,19 @@ func (c *Keba) currents() (float64, float64, float64, error) { return float64(kr.I1) / 1e3, float64(kr.I2) / 1e3, float64(kr.I3) / 1e3, err } -var _ api.Identifier = (*Keba)(nil) +var _ api.Identifier = (*KebaUdp)(nil) // Identify implements the api.Identifier interface -func (c *Keba) Identify() (string, error) { +func (c *KebaUdp) Identify() (string, error) { var kr keba.Report100 err := c.roundtrip("report", 100, &kr) return kr.RFIDTag, err } -var _ api.Diagnosis = (*Keba)(nil) +var _ api.Diagnosis = (*KebaUdp)(nil) // Diagnose implements the api.Diagnosis interface -func (c *Keba) Diagnose() { +func (c *KebaUdp) Diagnose() { var kr keba.Report100 if err := c.roundtrip("report", 100, &kr); err == nil { fmt.Printf("%+v\n", kr) diff --git a/charger/keba_decorators.go b/charger/keba-udp_decorators.go similarity index 57% rename from charger/keba_decorators.go rename to charger/keba-udp_decorators.go index 54cf8ff89..ce6597dc3 100644 --- a/charger/keba_decorators.go +++ b/charger/keba-udp_decorators.go @@ -6,104 +6,104 @@ import ( "github.com/evcc-io/evcc/api" ) -func decorateKeba(base *Keba, meter func() (float64, error), meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error)) api.Charger { +func decorateKebaUdp(base *KebaUdp, meter func() (float64, error), meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error)) api.Charger { switch { case meter == nil && meterEnergy == nil && phaseCurrents == nil: return base case meter != nil && meterEnergy == nil && phaseCurrents == nil: return &struct { - *Keba + *KebaUdp api.Meter }{ - Keba: base, - Meter: &decorateKebaMeterImpl{ + KebaUdp: base, + Meter: &decorateKebaUdpMeterImpl{ meter: meter, }, } case meter == nil && meterEnergy != nil && phaseCurrents == nil: return &struct { - *Keba + *KebaUdp api.MeterEnergy }{ - Keba: base, - MeterEnergy: &decorateKebaMeterEnergyImpl{ + KebaUdp: base, + MeterEnergy: &decorateKebaUdpMeterEnergyImpl{ meterEnergy: meterEnergy, }, } case meter != nil && meterEnergy != nil && phaseCurrents == nil: return &struct { - *Keba + *KebaUdp api.Meter api.MeterEnergy }{ - Keba: base, - Meter: &decorateKebaMeterImpl{ + KebaUdp: base, + Meter: &decorateKebaUdpMeterImpl{ meter: meter, }, - MeterEnergy: &decorateKebaMeterEnergyImpl{ + MeterEnergy: &decorateKebaUdpMeterEnergyImpl{ meterEnergy: meterEnergy, }, } case meter == nil && meterEnergy == nil && phaseCurrents != nil: return &struct { - *Keba + *KebaUdp api.PhaseCurrents }{ - Keba: base, - PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + KebaUdp: base, + PhaseCurrents: &decorateKebaUdpPhaseCurrentsImpl{ phaseCurrents: phaseCurrents, }, } case meter != nil && meterEnergy == nil && phaseCurrents != nil: return &struct { - *Keba + *KebaUdp api.Meter api.PhaseCurrents }{ - Keba: base, - Meter: &decorateKebaMeterImpl{ + KebaUdp: base, + Meter: &decorateKebaUdpMeterImpl{ meter: meter, }, - PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + PhaseCurrents: &decorateKebaUdpPhaseCurrentsImpl{ phaseCurrents: phaseCurrents, }, } case meter == nil && meterEnergy != nil && phaseCurrents != nil: return &struct { - *Keba + *KebaUdp api.MeterEnergy api.PhaseCurrents }{ - Keba: base, - MeterEnergy: &decorateKebaMeterEnergyImpl{ + KebaUdp: base, + MeterEnergy: &decorateKebaUdpMeterEnergyImpl{ meterEnergy: meterEnergy, }, - PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + PhaseCurrents: &decorateKebaUdpPhaseCurrentsImpl{ phaseCurrents: phaseCurrents, }, } case meter != nil && meterEnergy != nil && phaseCurrents != nil: return &struct { - *Keba + *KebaUdp api.Meter api.MeterEnergy api.PhaseCurrents }{ - Keba: base, - Meter: &decorateKebaMeterImpl{ + KebaUdp: base, + Meter: &decorateKebaUdpMeterImpl{ meter: meter, }, - MeterEnergy: &decorateKebaMeterEnergyImpl{ + MeterEnergy: &decorateKebaUdpMeterEnergyImpl{ meterEnergy: meterEnergy, }, - PhaseCurrents: &decorateKebaPhaseCurrentsImpl{ + PhaseCurrents: &decorateKebaUdpPhaseCurrentsImpl{ phaseCurrents: phaseCurrents, }, } @@ -112,26 +112,26 @@ func decorateKeba(base *Keba, meter func() (float64, error), meterEnergy func() return nil } -type decorateKebaMeterImpl struct { +type decorateKebaUdpMeterImpl struct { meter func() (float64, error) } -func (impl *decorateKebaMeterImpl) CurrentPower() (float64, error) { +func (impl *decorateKebaUdpMeterImpl) CurrentPower() (float64, error) { return impl.meter() } -type decorateKebaMeterEnergyImpl struct { +type decorateKebaUdpMeterEnergyImpl struct { meterEnergy func() (float64, error) } -func (impl *decorateKebaMeterEnergyImpl) TotalEnergy() (float64, error) { +func (impl *decorateKebaUdpMeterEnergyImpl) TotalEnergy() (float64, error) { return impl.meterEnergy() } -type decorateKebaPhaseCurrentsImpl struct { +type decorateKebaUdpPhaseCurrentsImpl struct { phaseCurrents func() (float64, float64, float64, error) } -func (impl *decorateKebaPhaseCurrentsImpl) Currents() (float64, float64, float64, error) { +func (impl *decorateKebaUdpPhaseCurrentsImpl) Currents() (float64, float64, float64, error) { return impl.phaseCurrents() } diff --git a/charger/keba/reports.go b/charger/keba/types.go similarity index 97% rename from charger/keba/reports.go rename to charger/keba/types.go index 246f9e085..697e8f8c4 100644 --- a/charger/keba/reports.go +++ b/charger/keba/types.go @@ -1,5 +1,10 @@ package keba +// RFID contains access credentials +type RFID struct { + Tag string +} + // Report contains report id and device serial type Report struct { ID int `json:"ID,string"` diff --git a/templates/definition/charger/keba-modbus.yaml b/templates/definition/charger/keba-modbus.yaml new file mode 100644 index 000000000..046ccd226 --- /dev/null +++ b/templates/definition/charger/keba-modbus.yaml @@ -0,0 +1,21 @@ +template: keba-modbus +products: + - brand: KEBA + description: + generic: KeContact P20, P30, C/X Series + - brand: BMW + description: + generic: i Wallbox +capabilities: ["1p3p", "mA", "rfid"] +requirements: + evcc: ["sponsorship"] + description: + de: Erfordert Firmwareversion 3.10.42 (C-series) bzw. 1.11 (X-series) + en: Requires firmware version 3.10.42 (C-series) bzw. 1.11 (X-series) +params: + - name: modbus + choice: ["tcpip"] + id: 255 +render: | + type: keba-modbus + {{- include "modbus" . }} diff --git a/templates/definition/charger/keba.yaml b/templates/definition/charger/keba-udp.yaml similarity index 89% rename from templates/definition/charger/keba.yaml rename to templates/definition/charger/keba-udp.yaml index ec5d7e481..74988c9eb 100644 --- a/templates/definition/charger/keba.yaml +++ b/templates/definition/charger/keba-udp.yaml @@ -2,10 +2,10 @@ template: keba products: - brand: KEBA description: - generic: KeContact P20, P30, C/X Series + generic: KeContact P20, P30, C/X Series (legacy UDP protocol) - brand: BMW description: - generic: i Wallbox + generic: i Wallbox (legacy UDP protocol) capabilities: ["mA", "rfid"] requirements: description: @@ -25,7 +25,7 @@ params: de: Die Seriennummer, ermöglicht es auch mit der Wallbox zu kommunizieren wenn evcc in Docker läuft. en: The serial number, allows to communicate with the Wallbox when running evcc in docker render: | - type: keba + type: keba-udp uri: {{ .host }} {{ if ne .rfid "" }} rfid: diff --git a/templates/docs/charger/keba-modbus_0.yaml b/templates/docs/charger/keba-modbus_0.yaml new file mode 100644 index 000000000..2ef1ce763 --- /dev/null +++ b/templates/docs/charger/keba-modbus_0.yaml @@ -0,0 +1,17 @@ +product: + brand: KEBA + description: KeContact P20, P30, C/X Series +capabilities: ["1p3p", "mA", "rfid"] +requirements: ["sponsorship"] +description: | + Erfordert Firmwareversion 3.10.42 (C-series) bzw. 1.11 (X-series) +render: + - default: | + type: template + template: keba-modbus + + # Modbus TCP + modbus: tcpip + id: 255 + host: 192.0.2.2 # Hostname + port: 502 # Port diff --git a/templates/docs/charger/keba-modbus_1.yaml b/templates/docs/charger/keba-modbus_1.yaml new file mode 100644 index 000000000..fa422a0e4 --- /dev/null +++ b/templates/docs/charger/keba-modbus_1.yaml @@ -0,0 +1,17 @@ +product: + brand: BMW + description: i Wallbox +capabilities: ["1p3p", "mA", "rfid"] +requirements: ["sponsorship"] +description: | + Erfordert Firmwareversion 3.10.42 (C-series) bzw. 1.11 (X-series) +render: + - default: | + type: template + template: keba-modbus + + # Modbus TCP + modbus: tcpip + id: 255 + host: 192.0.2.2 # Hostname + port: 502 # Port diff --git a/templates/docs/charger/keba_0.yaml b/templates/docs/charger/keba_0.yaml index 311578503..f39a334e9 100644 --- a/templates/docs/charger/keba_0.yaml +++ b/templates/docs/charger/keba_0.yaml @@ -1,6 +1,6 @@ product: brand: KEBA - description: KeContact P20, P30, C/X Series + description: KeContact P20, P30, C/X Series (legacy UDP protocol) capabilities: ["mA", "rfid"] description: | Es muss eine sogenannte UDP Funktion über den DIP Schalter 1.3 eingeschaltet (ON) werden. Die Installationsanleitung der Wallbox hilft hier weiter. diff --git a/templates/docs/charger/keba_1.yaml b/templates/docs/charger/keba_1.yaml index a5add0a27..cba7c7407 100644 --- a/templates/docs/charger/keba_1.yaml +++ b/templates/docs/charger/keba_1.yaml @@ -1,6 +1,6 @@ product: brand: BMW - description: i Wallbox + description: i Wallbox (legacy UDP protocol) capabilities: ["mA", "rfid"] description: | Es muss eine sogenannte UDP Funktion über den DIP Schalter 1.3 eingeschaltet (ON) werden. Die Installationsanleitung der Wallbox hilft hier weiter.