Voltie: only offer phase switching where the power board supports it (#33070)

This commit is contained in:
Voltie EV Charging Solutions 2026-08-22 13:48:55 +02:00 • committed by GitHub
parent ebe064ad97
commit fbbe12a0ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 30 additions and 12 deletions

View file

@ -55,6 +55,11 @@ import (
// communication timeout, the hardware current limit, lifetime energy and the // communication timeout, the hardware current limit, lifetime energy and the
// phase powers. Those capabilities are only offered when the charger reports // phase powers. Those capabilities are only offered when the charger reports
// that firmware, and the block read lengths follow the firmware as well. // that firmware, and the block read lengths follow the firmware as well.
//
// Phase switching additionally depends on the power board, which the firmware
// does not expose over Modbus. The same firmware also runs on older boards that
// have no L2/L3 relay and reject the write, so the capability is enabled by
// configuration rather than guessed.
const ( const (
// register blocks fetched in bulk // register blocks fetched in bulk
@ -267,6 +272,7 @@ func NewVoltieFromConfig(ctx context.Context, other map[string]any) (api.Charger
cc := struct { cc := struct {
modbus.TcpSettings `mapstructure:",squash"` modbus.TcpSettings `mapstructure:",squash"`
Cache time.Duration Cache time.Duration
Phases1p3p bool
}{ }{
TcpSettings: modbus.TcpSettings{ TcpSettings: modbus.TcpSettings{
ID: voltieDefaultSlaveID, ID: voltieDefaultSlaveID,
@ -280,11 +286,11 @@ func NewVoltieFromConfig(ctx context.Context, other map[string]any) (api.Charger
return nil, err return nil, err
} }
return NewVoltie(ctx, cc.TcpSettings, cc.Cache) return NewVoltie(ctx, cc.TcpSettings, cc.Cache, cc.Phases1p3p)
} }
// NewVoltie creates a Voltie charger // NewVoltie creates a Voltie charger
func NewVoltie(ctx context.Context, settings modbus.TcpSettings, cache time.Duration) (*Voltie, error) { func NewVoltie(ctx context.Context, settings modbus.TcpSettings, cache time.Duration, phaseSwitching bool) (*Voltie, error) {
conn, err := settings.Connection(ctx) conn, err := settings.Connection(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -329,11 +335,18 @@ func NewVoltie(ctx context.Context, settings modbus.TcpSettings, cache time.Dura
} }
if wb.ext { if wb.ext {
implement.Has(wb, implement.PhaseSwitcher(wb.phases1p3p))
implement.Has(wb, implement.PhaseGetter(wb.getPhases)) implement.Has(wb, implement.PhaseGetter(wb.getPhases))
implement.Has(wb, implement.MeterEnergy(wb.totalEnergy)) implement.Has(wb, implement.MeterEnergy(wb.totalEnergy))
implement.Has(wb, implement.PhasePowers(wb.powers)) implement.Has(wb, implement.PhasePowers(wb.powers))
implement.Has(wb, implement.CurrentLimiter(wb.getMinMaxCurrent)) implement.Has(wb, implement.CurrentLimiter(wb.getMinMaxCurrent))
if phaseSwitching {
implement.Has(wb, implement.PhaseSwitcher(wb.phases1p3p))
}
}
if phaseSwitching && !wb.ext {
log.WARN.Printf("phase switching requires firmware %d or later", voltieExtFirmware)
} }
return wb, nil return wb, nil
@ -533,12 +546,8 @@ func (wb *Voltie) Voltages() (float64, float64, float64, error) {
return wb.getPhaseValues(voltieRegVoltages, 1e3) return wb.getPhaseValues(voltieRegVoltages, 1e3)
} }
// phases1p3p implements the api.PhaseSwitcher interface. The register is only // phases1p3p implements the api.PhaseSwitcher interface. The firmware never
// writable on chargers manufactured from 2026, which carry an HPOW108 power // accepts the write while charging, so charging is paused for the switch.
// board; other hardware rejects the write with exception 0x03, as does a relay
// or EEPROM error. The hardware generation cannot be detected up front: the
// serial numbers exposed over Modbus are the MCU and power board serials, not
// the charger serial that carries the generation prefix.
func (wb *Voltie) phases1p3p(phases int) error { func (wb *Voltie) phases1p3p(phases int) error {
if phases != 1 && phases != 3 { if phases != 1 && phases != 3 {
return fmt.Errorf("invalid phases: %d", phases) return fmt.Errorf("invalid phases: %d", phases)
@ -565,9 +574,7 @@ func (wb *Voltie) phases1p3p(phases int) error {
u = 1 u = 1
} }
if _, err = wb.conn.WriteSingleRegister(voltieRegSinglePhase, u); err != nil { _, err = wb.conn.WriteSingleRegister(voltieRegSinglePhase, u)
err = fmt.Errorf("switch phases: %w", err)
}
wb.cache.Clear() wb.cache.Clear()
} }

View file

@ -26,7 +26,18 @@ params:
- name: cache - name: cache
advanced: true advanced: true
default: 1s default: 1s
- name: phases1p3p
description:
de: Phasenumschaltung
en: Phase switching
help:
de: Nur auf Ladegeräten ab Baujahr 2026 verfügbar, deren Seriennummer mit 1026 beginnt. Auf älterer Hardware weist das Ladegerät die Umschaltung ab.
en: Only available on chargers manufactured from 2026, whose serial number starts with 1026. On older hardware the charger rejects the switch.
type: bool
default: false
advanced: true
render: | render: |
type: voltie type: voltie
{{- include "modbus" . }} {{- include "modbus" . }}
cache: {{ .cache }} cache: {{ .cache }}
phases1p3p: {{ .phases1p3p }}