From b336c4d70dc902cf7c9bb3e1cb5314b7ef9b5ad7 Mon Sep 17 00:00:00 2001 From: wobu Date: Sun, 11 Aug 2024 13:49:12 +0200 Subject: [PATCH] Add NrgKick Gen2 (#15138) --- README.md | 2 +- charger/nrggen2.go | 336 ++++++++++++++++++++++ charger/nrggen2_decorators.go | 35 +++ templates/definition/charger/nrggen2.yaml | 26 ++ 4 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 charger/nrggen2.go create mode 100644 charger/nrggen2_decorators.go create mode 100644 templates/definition/charger/nrggen2.yaml diff --git a/README.md b/README.md index 3150a206b..75ddae353 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe - simple and clean user interface - wide range of supported [chargers](https://docs.evcc.io/docs/devices/chargers): - - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more + - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), NRGKick Gen2,[openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more - experimental EEBus support (Elli, PMCC) - experimental OCPP support - Build-your-own: Phoenix Contact (includes ESL Walli), [EVSE DIN](http://evracing.cz/simple-evse-wallbox) diff --git a/charger/nrggen2.go b/charger/nrggen2.go new file mode 100644 index 000000000..1c3c0ccde --- /dev/null +++ b/charger/nrggen2.go @@ -0,0 +1,336 @@ +package charger + +import ( + "encoding/binary" + "fmt" + "math" + + "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" + "github.com/spf13/cast" + "github.com/volkszaehler/mbmd/encoding" +) + +// https://www.nrgkick.com/wp-content/uploads/2024/07/local_api_docu_simulate.html + +// NRGKickGen2 charger implementation +type NRGKickGen2 struct { + conn *modbus.Connection +} + +const ( + // All register use LittleEndian + // Read only (0x03) + nrgKickGen2Serial = 0 // 11 regs + nrgKickGen2ModelType = 11 // 16 regs + nrgKickGen2MaxPhases = 36 + nrgKickGen2SoftwareVersionSM = 122 // 8 regs + // Read (0x03) / Write (0x06, 0x16) Registers + nrgKickGen2ChargingCurrent = 194 // A, factor 10 + nrgKickGen2Enabled = 195 + nrgKickGen2Phases = 198 + // Read only (0x03) + nrgKickGen2TotalChargedEnergy = 199 // Wh, 4 regs + nrgKickGen2ChargedEnergy = 203 // Wh, 2 regs + nrgKickGen2TotalActivePower = 210 // W, 2 regs, factor 1000 + nrgKickGen2PhaseVoltages = 217 // factor 100 + nrgKickGen2PhaseCurrents = 220 // factor 1000 + nrgKickGen2RegStatus = 251 + nrgKickGen2RegRelais = 253 + nrgKickGen2RegRCD = 255 + nrgKickGen2RegWarning = 256 + nrgKickGen2RegError = 257 +) + +func init() { + registry.Add("nrggen2", NewNRGKickGen2FromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decorateNRGKickGen2 -b *NRGKickGen2 -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" + +// NewNRGKickGen2FromConfig creates a NRGKickGen2 charger from generic config +func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + modbus.TcpSettings `mapstructure:",squash"` + Phases1p3p bool + }{ + TcpSettings: modbus.TcpSettings{ + ID: 1, // default + }, + Phases1p3p: false, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + nrg, err := NewNRGKickGen2(cc.URI, cc.ID) + if err != nil { + return nil, err + } + + var ( + phasesS func(int) error + ) + + if cc.Phases1p3p { + // user could have an adapter plug which doesn't support 3 phases + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2MaxPhases, 1); err == nil { + if maxPhases := encoding.Uint16(b); maxPhases > 1 { + phasesS = nrg.phases1p3p + } + } + } + + return decorateNRGKickGen2(nrg, phasesS), nil +} + +// NewNRGKickGen2 creates NRGKickGen2 charger +func NewNRGKickGen2(uri string, slaveID uint8) (*NRGKickGen2, 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("nrggen2") + conn.Logger(log.TRACE) + + nrg := &NRGKickGen2{ + conn: conn, + } + + return nrg, nil +} + +// Status implements the api.Charger interface +func (nrg *NRGKickGen2) Status() (api.ChargeStatus, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegStatus, 1) + if err != nil { + return api.StatusNone, err + } + + // 0 - "UNKNOWN", + // 1 - "STANDBY", + // 2 - "CONNECTED", + // 3 - "CHARGING", + // 6 - "ERROR", + // 7 - "WAKEUP" + switch status := binary.BigEndian.Uint16(b); status { + case 0: + return api.StatusNone, nil + case 1: + return api.StatusA, nil + case 2: + return api.StatusB, nil + case 3: + return api.StatusC, nil + case 6: + // 0 - "NO_ERROR", + // 1 - "GENERAL_ERROR", + // 2 - "32A_ATTACHMENT_ON_16A_UNIT", + // 3 - "VOLTAGE_DROP_DETECTED", + // 4 - "UNPLUG_DETECTION_TRIGGERED", + // 5 - "TYPE2_NOT_AUTHORIZED", + // 16 - "RESIDUAL_CURRENT_DETECTED", + // 32 - "CP_SIGNAL_VOLTAGE_ERROR", + // 33 - "CP_SIGNAL_IMPERMISSIBLE", + // 34 - "EV_DIODE_FAULT", + // 48 - "PE_SELF_TEST_FAILED", + // 49 - "RCD_SELF_TEST_FAILED", + // 50 - "RELAY_SELF_TEST_FAILED", + // 51 - "PE_AND_RCD_SELF_TEST_FAILED", + // 52 - "PE_AND_RELAY_SELF_TEST_FAILED", + // 53 - "RCD_AND_RELAY_SELF_TEST_FAILED", + // 54 - "PE_AND_RCD_AND_RELAY_SELF_TEST_FAILED", + // 64 - "SUPPLY_VOLTAGE_ERROR", + // 65 - "PHASE_SHIFT_ERROR", + // 66 - "OVERVOLTAGE_DETECTED", + // 67 - "UNDERVOLTAGE_DETECTED", + // 68 - "OVERVOLTAGE_WITHOUT_PE_DETECTED", + // 69 - "UNDERVOLTAGE_WITHOUT_PE_DETECTED", + // 70 - "UNDERFREQUENCY_DETECTED", + // 71 - "OVERFREQUENCY_DETECTED", + // 72 - "UNKNOWN_FREQUENCY_TYPE", + // 73 - "UNKNOWN_GRID_TYPE", + // 80 - "GENERAL_OVERTEMPERATURE", + // 81 - "HOUSING_OVERTEMPERATURE", + // 82 - "ATTACHMENT_OVERTEMPERATURE", + // 83 - "DOMESTIC_PLUG_OVERTEMPERATURE", + // x - "UNKNOWN" + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegError, 1) + if err != nil { + return api.StatusNone, err + } + return api.StatusF, fmt.Errorf("%d", error) + case 7: + return api.StatusB, nil + default: + return api.StatusNone, fmt.Errorf("unhandled status type") + } +} + +// Enabled implements the api.Charger interface +func (nrg *NRGKickGen2) Enabled() (bool, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Enabled, 1) + if err != nil { + return false, err + } + + // 0 = no charge pause, 1 = charge pause + return binary.BigEndian.Uint16(b) == 0, nil +} + +// Enable implements the api.Charger interface +func (nrg *NRGKickGen2) Enable(enable bool) error { + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2Enabled, cast.ToUint16(!enable)) + return err +} + +// MaxCurrent implements the api.Charger interface +func (nrg *NRGKickGen2) MaxCurrent(current int64) error { + return nrg.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*NRGKickGen2)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (nrg *NRGKickGen2) MaxCurrentMillis(current float64) error { + if current < 6 { + return fmt.Errorf("invalid current %.1f", current) + } + + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2ChargingCurrent, uint16(math.Trunc(current*10))) + return err +} + +func (nrg *NRGKickGen2) GetMaxCurrent() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ChargingCurrent, 1) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint16(b)) / 10, nil +} + +var _ api.Meter = (*NRGKickGen2)(nil) + +// CurrentPower implements the api.Meter interface +func (nrg *NRGKickGen2) CurrentPower() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2TotalActivePower, 2) + if err != nil { + return 0, err + } + + return float64(encoding.Int32LswFirst(b)) * 1e-3, nil +} + +var _ api.MeterEnergy = (*NRGKickGen2)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (nrg *NRGKickGen2) TotalEnergy() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2TotalChargedEnergy, 4) + if err != nil { + return 0, err + } + + return float64(encoding.Uint64LswFirst(b)) * 1e-3, nil +} + +var _ api.PhaseCurrents = (*NRGKickGen2)(nil) + +// Currents implements the api.PhaseCurrents interface +func (nrg *NRGKickGen2) Currents() (float64, float64, float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2PhaseCurrents, 3) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) * 1e-3 + } + + return res[0], res[1], res[2], nil +} + +var _ api.PhaseVoltages = (*NRGKickGen2)(nil) + +// Currents implements the api.PhaseVoltages interface +func (nrg *NRGKickGen2) Voltages() (float64, float64, float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2PhaseVoltages, 3) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) * 1e-2 + } + + return res[0], res[1], res[2], nil +} + +var _ api.ChargeRater = (*NRGKickGen2)(nil) + +func (nrg *NRGKickGen2) ChargedEnergy() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ChargedEnergy, 2) + if err != nil { + return 0, err + } + + return float64(encoding.Uint32LswFirst(b)) * 1e-3, nil +} + +// Phases1p3p implements the api.PhaseSwitcher interface +func (nrg *NRGKickGen2) phases1p3p(phases int) error { + // this can return an error, if phase switching isn't activated via the App + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2Phases, uint16(phases)) + return err +} + +var _ api.PhaseGetter = (*NRGKickGen2)(nil) + +func (nrg *NRGKickGen2) GetPhases() (int, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Phases, 1) + if err != nil { + return 0, err + } + + return int(binary.BigEndian.Uint16(b)), nil +} + +var _ api.Diagnosis = (*NRGKickGen2)(nil) + +// Diagnose implements the api.Diagnosis interface +func (nrg *NRGKickGen2) Diagnose() { + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Serial, 11); err == nil { + fmt.Printf("\tSerial:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ModelType, 16); err == nil { + fmt.Printf("\tModel:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2SoftwareVersionSM, 8); err == nil { + fmt.Printf("\tSmartModule Version:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegStatus, 1); err == nil { + fmt.Printf("\tStatus:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegRelais, 1); err == nil { + fmt.Printf("\tRelais Switching:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegRCD, 1); err == nil { + fmt.Printf("\tRCD:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegWarning, 1); err == nil { + fmt.Printf("\tWarning:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegError, 1); err == nil { + fmt.Printf("\tError:\t%d\n", binary.BigEndian.Uint16(b)) + } +} diff --git a/charger/nrggen2_decorators.go b/charger/nrggen2_decorators.go new file mode 100644 index 000000000..c2db71ca9 --- /dev/null +++ b/charger/nrggen2_decorators.go @@ -0,0 +1,35 @@ +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 decorateNRGKickGen2(base *NRGKickGen2, phaseSwitcher func(int) error) api.Charger { + switch { + case phaseSwitcher == nil: + return base + + case phaseSwitcher != nil: + return &struct { + *NRGKickGen2 + api.PhaseSwitcher + }{ + NRGKickGen2: base, + PhaseSwitcher: &decorateNRGKickGen2PhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decorateNRGKickGen2PhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateNRGKickGen2PhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} diff --git a/templates/definition/charger/nrggen2.yaml b/templates/definition/charger/nrggen2.yaml new file mode 100644 index 000000000..aa5ac0ac6 --- /dev/null +++ b/templates/definition/charger/nrggen2.yaml @@ -0,0 +1,26 @@ +template: nrggen2 +products: + - brand: NRGKick + description: + generic: Gen2 +requirements: + evcc: ["sponsorship"] +capabilities: ["1p3p", "mA"] +params: + - name: modbus + choice: ["tcpip"] + id: 1 + - name: phases1p3p + type: bool + default: false + advanced: true + help: + de: Aktiviert Phasenumschaltung. Erweiterte Funktion "Phasenumschaltung" muss in der NRGkick App aktiviert sein. + en: Activates phase switching. Extended feature "Phase Switching" must be activated in the NRGKick app. +render: | + type: nrggen2 + {{- include "modbus" . }} + {{- if ne .phases1p3p "false" }} + phases1p3p: true + {{- end }} +