Add Em2Go Duo Power charger support
- Add em2go-duo-power.yaml template for dual-port AC WALLBOX DUO POWER 2X 22KW - Extend em2go.go implementation to support dual connector addressing - Add connector-based register offset (100 registers per connector) - Support connector 1 and 2 configuration via template parameter Addresses GitHub issue #22518 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b7015af551
commit
18c49ef45d
2 changed files with 106 additions and 25 deletions
108
charger/em2go.go
108
charger/em2go.go
|
|
@ -38,6 +38,7 @@ type Em2Go struct {
|
|||
current uint16
|
||||
workaround bool
|
||||
phases int
|
||||
base uint16 // register base offset for dual connector support
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -60,6 +61,9 @@ const (
|
|||
em2GoRegVoltages = 109 // Uint16 RO 0.1V
|
||||
em2GoRegPhases = 200 // Set charging phase 1 unsigned
|
||||
|
||||
// Duo Power connector offset - connector 2 registers start at offset 100
|
||||
em2GoDuoPowerOffset = 100
|
||||
|
||||
//removed due to unreliable session energy when pausing or switching phases
|
||||
//em2GoRegChargedEnergy = 72 // Uint16 RO 0.1kWh
|
||||
)
|
||||
|
|
@ -67,6 +71,7 @@ const (
|
|||
func init() {
|
||||
registry.AddCtx("em2go", NewEm2GoFromConfig)
|
||||
registry.AddCtx("em2go-home", NewEm2GoFromConfig)
|
||||
registry.AddCtx("em2go-duo-power", NewEm2GoDuoPowerFromConfig)
|
||||
}
|
||||
|
||||
//go:generate go tool decorate -f decorateEm2Go -b *Em2Go -r api.Charger -t "api.ChargerEx,MaxCurrentMillis,func(float64) error" -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)"
|
||||
|
|
@ -84,6 +89,23 @@ func NewEm2GoFromConfig(ctx context.Context, other map[string]interface{}) (api.
|
|||
return NewEm2Go(ctx, cc.URI, cc.ID)
|
||||
}
|
||||
|
||||
// NewEm2GoDuoPowerFromConfig creates a Em2Go Duo Power charger from generic config
|
||||
func NewEm2GoDuoPowerFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
|
||||
cc := struct {
|
||||
modbus.TcpSettings `mapstructure:",squash"`
|
||||
Connector int
|
||||
}{
|
||||
TcpSettings: modbus.TcpSettings{ID: 255},
|
||||
Connector: 1,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewEm2GoDuoPower(ctx, cc.URI, cc.ID, cc.Connector)
|
||||
}
|
||||
|
||||
// NewEm2Go creates Em2Go charger
|
||||
func NewEm2Go(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) {
|
||||
uri = util.DefaultPort(uri, 502)
|
||||
|
|
@ -104,8 +126,44 @@ func NewEm2Go(ctx context.Context, uri string, slaveID uint8) (api.Charger, erro
|
|||
conn: conn,
|
||||
current: 60,
|
||||
workaround: false,
|
||||
base: 0, // single connector, no offset
|
||||
}
|
||||
|
||||
return wb.initialize()
|
||||
}
|
||||
|
||||
// NewEm2GoDuoPower creates Em2Go Duo Power charger
|
||||
func NewEm2GoDuoPower(ctx context.Context, uri string, slaveID uint8, connector int) (api.Charger, error) {
|
||||
if connector < 1 || connector > 2 {
|
||||
return nil, fmt.Errorf("invalid connector: %d", connector)
|
||||
}
|
||||
|
||||
uri = util.DefaultPort(uri, 502)
|
||||
|
||||
conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add delay of 60 milliseconds between requests
|
||||
conn.Delay(60 * time.Millisecond)
|
||||
|
||||
log := util.NewLogger("em2go-duo-power")
|
||||
conn.Logger(log.TRACE)
|
||||
|
||||
wb := &Em2Go{
|
||||
log: log,
|
||||
conn: conn,
|
||||
current: 60,
|
||||
workaround: false,
|
||||
base: uint16((connector - 1) * em2GoDuoPowerOffset),
|
||||
}
|
||||
|
||||
return wb.initialize()
|
||||
}
|
||||
|
||||
// initialize performs common initialization for both Em2Go models
|
||||
func (wb *Em2Go) initialize() (api.Charger, error) {
|
||||
var (
|
||||
maxCurrent func(float64) error
|
||||
phases1p3p func(int) error
|
||||
|
|
@ -129,7 +187,7 @@ func NewEm2Go(ctx context.Context, uri string, slaveID uint8) (api.Charger, erro
|
|||
maxCurrent = wb.maxCurrentMillis
|
||||
}
|
||||
|
||||
if _, err := wb.conn.ReadHoldingRegisters(em2GoRegPhases, 1); err == nil {
|
||||
if _, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegPhases, 1); err == nil {
|
||||
phases1p3p = wb.phases1p3p
|
||||
phasesG = wb.getPhases
|
||||
}
|
||||
|
|
@ -139,7 +197,7 @@ func NewEm2Go(ctx context.Context, uri string, slaveID uint8) (api.Charger, erro
|
|||
|
||||
// Status implements the api.Charger interface
|
||||
func (wb *Em2Go) Status() (api.ChargeStatus, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegStatus, 1)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegStatus, 1)
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
|
@ -162,7 +220,7 @@ func (wb *Em2Go) Status() (api.ChargeStatus, error) {
|
|||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (wb *Em2Go) Enabled() (bool, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegChargeCommand, 1)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegChargeCommand, 1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
@ -177,7 +235,7 @@ func (wb *Em2Go) Enable(enable bool) error {
|
|||
b := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(b, map[bool]uint16{true: 1, false: 2}[enable])
|
||||
|
||||
if _, err := wb.conn.WriteMultipleRegisters(em2GoRegChargeCommand, 1, b); err != nil {
|
||||
if _, err := wb.conn.WriteMultipleRegisters(wb.base+em2GoRegChargeCommand, 1, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +243,7 @@ func (wb *Em2Go) Enable(enable bool) error {
|
|||
if wb.workaround && enable {
|
||||
if wb.phases == 1 {
|
||||
binary.BigEndian.PutUint16(b, uint16(wb.phases))
|
||||
if _, err := wb.conn.WriteMultipleRegisters(em2GoRegPhases, 1, b); err != nil {
|
||||
if _, err := wb.conn.WriteMultipleRegisters(wb.base+em2GoRegPhases, 1, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +259,7 @@ func (wb *Em2Go) setCurrent(current uint16) error {
|
|||
b := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(b, current)
|
||||
|
||||
_, err := wb.conn.WriteMultipleRegisters(em2GoRegCurrentLimit, 1, b)
|
||||
_, err := wb.conn.WriteMultipleRegisters(wb.base+em2GoRegCurrentLimit, 1, b)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +282,7 @@ var _ api.CurrentGetter = (*Em2Go)(nil)
|
|||
|
||||
// GetMaxCurrent implements the api.CurrentGetter interface
|
||||
func (wb Em2Go) GetMaxCurrent() (float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegCurrentLimit, 1)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegCurrentLimit, 1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -236,7 +294,7 @@ var _ api.Meter = (*Em2Go)(nil)
|
|||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
func (wb *Em2Go) CurrentPower() (float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegPower, 2)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegPower, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -248,7 +306,7 @@ var _ api.MeterEnergy = (*Em2Go)(nil)
|
|||
|
||||
// TotalEnergy implements the api.MeterEnergy interface
|
||||
func (wb *Em2Go) TotalEnergy() (float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegEnergy, 2)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegEnergy, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -261,7 +319,7 @@ func (wb *Em2Go) getPhaseValues(reg uint16) (float64, float64, float64, error) {
|
|||
var res [3]float64
|
||||
|
||||
for i := range 3 {
|
||||
b, err := wb.conn.ReadHoldingRegisters(reg+2*uint16(i), 1)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+reg+2*uint16(i), 1)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
|
@ -290,7 +348,7 @@ var _ api.ChargeTimer = (*Em2Go)(nil)
|
|||
|
||||
// ChargeDuration implements the api.ChargeTimer interface
|
||||
func (wb *Em2Go) ChargeDuration() (time.Duration, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegChargeDuration, 2)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegChargeDuration, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -323,7 +381,7 @@ func (wb *Em2Go) phases1p3p(phases int) error {
|
|||
b := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(b, uint16(phases))
|
||||
|
||||
_, err := wb.conn.WriteMultipleRegisters(em2GoRegPhases, 1, b)
|
||||
_, err := wb.conn.WriteMultipleRegisters(wb.base+em2GoRegPhases, 1, b)
|
||||
if err == nil {
|
||||
wb.phases = phases
|
||||
}
|
||||
|
|
@ -333,7 +391,7 @@ func (wb *Em2Go) phases1p3p(phases int) error {
|
|||
|
||||
// getPhases implements the api.PhaseGetter interface
|
||||
func (wb *Em2Go) getPhases() (int, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegPhases, 1)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegPhases, 1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -345,46 +403,46 @@ var _ api.Diagnosis = (*Em2Go)(nil)
|
|||
|
||||
// Diagnose implements the api.Diagnosis interface
|
||||
func (wb *Em2Go) Diagnose() {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegStatus, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegStatus, 1); err == nil {
|
||||
fmt.Printf("\tCharging Station Status:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegConnectorState, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegConnectorState, 1); err == nil {
|
||||
fmt.Printf("\tConnector State:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegErrorCode, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegErrorCode, 1); err == nil {
|
||||
fmt.Printf("\tError Code:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegMaxCurrent, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegMaxCurrent, 1); err == nil {
|
||||
fmt.Printf("\tEVSE Max. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegMaxCurrent, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegMinCurrent, 1); err == nil {
|
||||
fmt.Printf("\tEVSE Min. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegCableMaxCurrent, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegCableMaxCurrent, 1); err == nil {
|
||||
fmt.Printf("\tCable Max. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10))
|
||||
}
|
||||
var serial []byte
|
||||
for reg := 0; reg < 8; reg++ {
|
||||
b, err := wb.conn.ReadHoldingRegisters(em2GoRegSerial+2*uint16(reg), 2)
|
||||
b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegSerial+2*uint16(reg), 2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
serial = append(serial, b...)
|
||||
}
|
||||
fmt.Printf("\tSerial: %s\n", string(serial))
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegSafeCurrent, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegSafeCurrent, 1); err == nil {
|
||||
fmt.Printf("\tSafe Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegCommTimeout, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegCommTimeout, 1); err == nil {
|
||||
fmt.Printf("\tConnection Timeout:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegCurrentLimit, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegCurrentLimit, 1); err == nil {
|
||||
fmt.Printf("\tCurrent Limit:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegChargeMode, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegChargeMode, 1); err == nil {
|
||||
fmt.Printf("\tCharge Mode:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(em2GoRegChargeCommand, 1); err == nil {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(wb.base+em2GoRegChargeCommand, 1); err == nil {
|
||||
fmt.Printf("\tCharge Command:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
templates/definition/charger/em2go-duo-power.yaml
Normal file
23
templates/definition/charger/em2go-duo-power.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
template: em2go-duo-power
|
||||
products:
|
||||
- brand: EM2GO
|
||||
description:
|
||||
generic: AC WALLBOX DUO POWER 2X 22KW
|
||||
capabilities: ["mA"]
|
||||
requirements:
|
||||
description:
|
||||
de: "Aktuelle Firmware mit Modbus-Unterstützung notwendig. Mögliche Verbindungsprobleme - Device ID muss bei jeder Verbindung inkrementiert werden."
|
||||
en: "Recent firmware with Modbus support required. Possible connection issues - Device ID needs to be incremented for each connection attempt."
|
||||
params:
|
||||
- name: modbus
|
||||
choice: ["tcpip"]
|
||||
id: 255
|
||||
- name: connector
|
||||
description:
|
||||
de: "Anschluss (1 oder 2 für Duo Power)"
|
||||
en: "Connector (1 or 2 for Duo Power)"
|
||||
default: 1
|
||||
render: |
|
||||
type: em2go-duo-power
|
||||
{{- include "modbus" . }}
|
||||
connector: {{ .connector }}
|
||||
Loading…
Add table
Add a link
Reference in a new issue