Skip to content

Commit

Permalink
Add NrgKick Gen2 (evcc-io#15138)
Browse files Browse the repository at this point in the history
  • Loading branch information
wobu authored Aug 11, 2024
1 parent 061beab commit b336c4d
Show file tree
Hide file tree
Showing 4 changed files with 398 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
336 changes: 336 additions & 0 deletions charger/nrggen2.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
35 changes: 35 additions & 0 deletions charger/nrggen2_decorators.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b336c4d

Please sign in to comment.