Add NrgKick Gen2 (#15138)
This commit is contained in:
parent
061beab082
commit
b336c4d70d
4 changed files with 398 additions and 1 deletions
|
|
@ -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)
|
||||
|
|
|
|||
336
charger/nrggen2.go
Normal file
336
charger/nrggen2.go
Normal file
|
|
@ -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
charger/nrggen2_decorators.go
Normal file
35
charger/nrggen2_decorators.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
26
templates/definition/charger/nrggen2.yaml
Normal file
26
templates/definition/charger/nrggen2.yaml
Normal file
|
|
@ -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 }}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue