From 420a0e9f9b95bc64efdafa3ada098a3404ffb2cd Mon Sep 17 00:00:00 2001 From: Eric Nieuwenhuijsen Date: Sun, 22 Mar 2026 11:07:14 +0100 Subject: [PATCH] Victron: add phase switching (#28408) --- charger/victron.go | 85 ++++++++++++++----- charger/victron_decorators.go | 58 +++++++++++++ .../definition/charger/victron-evcs.yaml | 13 ++- 3 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 charger/victron_decorators.go diff --git a/charger/victron.go b/charger/victron.go index 42090db7a..f331612bb 100644 --- a/charger/victron.go +++ b/charger/victron.go @@ -36,13 +36,15 @@ type Victron struct { } type victronRegs struct { - Mode uint16 - Energy uint16 - Power uint16 - Status uint16 - SetCurrent uint16 - Enabled uint16 - isGX bool + Mode uint16 + Energy uint16 + Power uint16 + Status uint16 + SetCurrent uint16 + Enabled uint16 + PhaseSwitch uint16 + ThreePhaseEnabled uint16 + isGX bool } var ( @@ -57,13 +59,15 @@ var ( } victronEVCS = victronRegs{ - Mode: 5009, - Energy: 5021, - Power: 5014, - Status: 5015, - SetCurrent: 5016, - Enabled: 5010, - isGX: false, + Mode: 5009, + Energy: 5021, + Power: 5014, + Status: 5015, + SetCurrent: 5016, + Enabled: 5010, + PhaseSwitch: 5055, + ThreePhaseEnabled: 5100, + isGX: false, } ) @@ -72,17 +76,19 @@ func init() { registry.AddCtx("victron-evcs", NewVictronEVCSFromConfig) } -// NewVictronGXFromConfig creates a ABB charger from generic config +//go:generate go tool decorate -f decorateVictron -b *Victron -r api.Charger -t api.PhaseSwitcher,api.PhaseGetter + +// NewVictronGXFromConfig creates a Victron charger from generic config func NewVictronGXFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { return NewVictronFromConfig(ctx, other, victronGX) } -// NewVictronEVCSFromConfig creates a ABB charger from generic config +// NewVictronEVCSFromConfig creates a Victron charger from generic config func NewVictronEVCSFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { return NewVictronFromConfig(ctx, other, victronEVCS) } -// NewVictronFromConfig creates a ABB charger from generic config +// NewVictronFromConfig creates a Victron charger from generic config func NewVictronFromConfig(ctx context.Context, other map[string]any, regs victronRegs) (api.Charger, error) { cc := modbus.TcpSettings{ ID: cast.ToUint8(regs.isGX) * 100, @@ -119,7 +125,23 @@ func NewVictron(ctx context.Context, uri string, slaveID uint8, regs victronRegs return nil, errors.New("charger must be in manual mode") } - return wb, nil + // phase switching (EVCS only, requires hardware modification with second contactor) + var phasesS func(int) error + var phasesG func() (int, error) + + if !regs.isGX { + b, err := wb.conn.ReadHoldingRegisters(wb.regs.ThreePhaseEnabled, 1) + if err != nil { + return nil, err + } + + if binary.BigEndian.Uint16(b) == 1 { + phasesS = wb.phases1p3p + phasesG = wb.getPhases + } + } + + return decorateVictron(wb, phasesS, phasesG), nil } // Status implements the api.Charger interface @@ -132,7 +154,7 @@ func (wb *Victron) Status() (api.ChargeStatus, error) { u := binary.BigEndian.Uint16(b) switch u { case 0, 1, 2, 3: - return api.ChargeStatusString(string('A' + rune(binary.BigEndian.Uint16(b)))) + return api.ChargeStatusString(string('A' + rune(u))) case 5, 6, 21: return api.StatusB, nil default: @@ -194,3 +216,28 @@ func (wb *Victron) ChargedEnergy() (float64, error) { return float64(binary.BigEndian.Uint16(b)) / 100, nil } + +// phases1p3p implements the api.PhaseSwitcher interface +func (wb *Victron) phases1p3p(phases int) error { + // register 5055: 1 = single phase, 0 = three phase + var u uint16 + if phases == 1 { + u = 1 + } + + _, err := wb.conn.WriteSingleRegister(wb.regs.PhaseSwitch, u) + return err +} + +// getPhases implements the api.PhaseGetter interface +func (wb *Victron) getPhases() (int, error) { + b, err := wb.conn.ReadHoldingRegisters(wb.regs.PhaseSwitch, 1) + if err != nil { + return 0, err + } + + if binary.BigEndian.Uint16(b) == 1 { + return 1, nil + } + return 3, nil +} diff --git a/charger/victron_decorators.go b/charger/victron_decorators.go new file mode 100644 index 000000000..7a3ea9b44 --- /dev/null +++ b/charger/victron_decorators.go @@ -0,0 +1,58 @@ +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 decorateVictron(base *Victron, phaseSwitcher func(int) error, phaseGetter func() (int, error)) api.Charger { + switch { + case phaseSwitcher == nil: + return base + + case phaseGetter == nil && phaseSwitcher != nil: + return &struct { + *Victron + api.PhaseSwitcher + }{ + Victron: base, + PhaseSwitcher: &decorateVictronPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case phaseGetter != nil && phaseSwitcher != nil: + return &struct { + *Victron + api.PhaseGetter + api.PhaseSwitcher + }{ + Victron: base, + PhaseGetter: &decorateVictronPhaseGetterImpl{ + phaseGetter: phaseGetter, + }, + PhaseSwitcher: &decorateVictronPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decorateVictronPhaseGetterImpl struct { + phaseGetter func() (int, error) +} + +func (impl *decorateVictronPhaseGetterImpl) GetPhases() (int, error) { + return impl.phaseGetter() +} + +type decorateVictronPhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateVictronPhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} diff --git a/templates/definition/charger/victron-evcs.yaml b/templates/definition/charger/victron-evcs.yaml index c1cc3b4f1..1fb951039 100644 --- a/templates/definition/charger/victron-evcs.yaml +++ b/templates/definition/charger/victron-evcs.yaml @@ -3,10 +3,19 @@ products: - brand: Victron description: generic: EV Charging Station +capabilities: ["1p3p"] requirements: description: - en: Enter the host of the charger (not the GX device) and ensure that the charger is in manual mode. - de: Trage den Host der Wallbox (nicht des GX-Geräts) ein und stelle sicher, dass die Wallbox sich im Modus "Manual" befindet. + en: | + Enter the host of the charger (not the GX device) and ensure that the charger is in manual mode. + + To enable 1P/3P phase switching a hardware modification is required: the 4P contactor must be replaced with 2×2P contactors. The first contactor switches L1+N, the second switches L2+L3 and is controlled by the second relay on the PCB. Register 5100 must be set to 1 to enable phase switching. + [More info](https://community.victronenergy.com/t/charging-an-ev-with-excess-solar-switching-between-1p-and-3p/21181) + de: | + Trage den Host der Wallbox (nicht des GX-Geräts) ein und stelle sicher, dass die Wallbox sich im Modus "Manual" befindet. + + Für die 1P/3P Phasenumschaltung ist eine Hardware-Modifikation erforderlich: Der 4P-Schütz muss durch 2×2P-Schütze ersetzt werden. Der erste Schütz schaltet L1+N, der zweite L2+L3 und wird über das zweite Relais auf der Platine gesteuert. Register 5100 muss auf 1 gesetzt werden, um die Phasenumschaltung zu aktivieren. + [Mehr Infos](https://community.victronenergy.com/t/charging-an-ev-with-excess-solar-switching-between-1p-and-3p/21181) params: - name: modbus choice: ["tcpip"]