diff --git a/charger/solax.go b/charger/solax.go index d52065355..38473089d 100644 --- a/charger/solax.go +++ b/charger/solax.go @@ -21,6 +21,7 @@ import ( "context" "encoding/binary" "fmt" + "strings" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" @@ -38,24 +39,35 @@ type Solax struct { const ( // holding (FC 0x03, 0x06, 0x10) - solaxRegDeviceMode = 0x060D // uint16 - solaxRegStartChargeMode = 0x0610 // uint16 - solaxRegPhases = 0x0625 // uint16 - solaxRegCommandControl = 0x0627 // uint16 - solaxRegMaxCurrent = 0x0628 // uint16 0.01A + solaxRegSerialNumber = 0x0600 // 7x string + solaxRegDeviceMode = 0x060D // uint16 + solaxRegCommandControl = 0x0627 // uint16 + solaxRegMaxCurrent = 0x0628 // uint16 0.01A + solaxRegPhaseSwitch = 0xA105 // uint16 // input (FC 0x04) - solaxRegVoltages = 0x0000 // 3x uint16 0.01V - solaxRegCurrents = 0x0004 // 3x uint16 0.01A - solaxRegActivePower = 0x000B // uint16 1W - solaxRegTotalEnergy = 0x0010 // uint32s 0.1kWh - solaxRegState = 0x001D // uint16 + solaxRegVoltages = 0x0000 // 3x uint16 0.01V + solaxRegCurrents = 0x0004 // 3x uint16 0.01A + solaxRegActivePower = 0x000B // uint16 1W + solaxRegTotalEnergy = 0x0010 // uint32 0.1kWh + solaxRegState = 0x001D // uint16 + solaxRegFaultCode = 0x001E // 2x uint32 + solaxRegFirmwareVersion = 0x0025 // uint16 Vx.xx + solaxRegConnectionStrength = 0x0027 // uint16 1% + solaxRegLockState = 0x002D // uint16 + solaxRegPhases = 0xA02A // uint16 + // commands solaxCmdStop = 3 solaxCmdStart = 4 + // modes solaxModeStop = 0 solaxModeFast = 1 + solaxModeECO = 2 + + // minimum firmware version for phase switching support + solaxFirmwarePhaseSwitching = 905 ) func init() { @@ -63,6 +75,8 @@ func init() { registry.AddCtx("solax-g2", NewSolaxG2FromConfig) } +//go:generate go tool decorate -f decorateSolax -b *Solax -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)" + func NewSolaxG1FromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { return NewSolaxFromConfig(ctx, other, true) } @@ -104,7 +118,18 @@ func NewSolax(ctx context.Context, uri, device, comset string, baudrate int, pro isLegacyHw: isLegacyHw, } - return wb, err + var phases1p3p func(int) error + var phasesG func() (int, error) + + if b, err := wb.conn.ReadInputRegisters(solaxRegFirmwareVersion, 1); err == nil { + v := encoding.Uint16(b) + if !wb.isLegacyHw && v >= solaxFirmwarePhaseSwitching { + phases1p3p = wb.phases1p3p + phasesG = wb.getPhases + } + } + + return decorateSolax(wb, phases1p3p, phasesG), nil } // getPhaseValues returns 3 sequential register values @@ -135,13 +160,19 @@ func (wb *Solax) Status() (api.ChargeStatus, error) { 5: // "Unavailable" return api.StatusA, nil case - 1, // "Preparing" - 8, // "SuspendedEVSE" - 7, // "SuspendedEV" - 3: // "Finishing" + 1, // "Preparing" + 3, // "Finishing" + 7, // "SuspendedEV" + 8, // "SuspendedEVSE" + 11, // "StartDelay" + 12, // "ChargPause" + 13, // "Stopping" + 17: // "PhaseSwitching" return api.StatusB, nil case 2: // "Charging" return api.StatusC, nil + case 4: // "Fault" + return api.StatusE, nil default: return api.StatusNone, fmt.Errorf("invalid status: %d", s) } @@ -228,19 +259,86 @@ func (wb *Solax) Voltages() (float64, float64, float64, error) { return wb.getPhaseValues(solaxRegVoltages) } -/* https://github.com/evcc-io/evcc/pull/14108 -var _ api.PhaseSwitcher = (*Solax)(nil) - -// Phases1p3p implements the api.PhaseSwitcher interface -func (wb *Solax) Phases1p3p(phases int) error { - var u uint16 - - if phases == 1 { - u = 1 +// phases1p3p implements the api.PhaseSwitcher interface +func (wb *Solax) phases1p3p(phases int) error { + u := uint16(1) + if phases == 3 { + u = 2 } - _, err := wb.conn.WriteSingleRegister(solaxRegPhases, u) + _, err := wb.conn.WriteSingleRegister(solaxRegPhaseSwitch, u) return err } -*/ + +// getPhases implements the api.PhaseGetter interface +func (wb *Solax) getPhases() (int, error) { + b, err := wb.conn.ReadInputRegisters(solaxRegPhases, 1) + if err != nil { + return 0, err + } + + switch binary.BigEndian.Uint16(b) { + case 1: + return 1, nil + case 2: + return 3, nil + default: + return 0, nil + } +} + +var _ api.Diagnosis = (*Solax)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *Solax) Diagnose() { + if b, err := wb.conn.ReadHoldingRegisters(solaxRegSerialNumber, 7); err == nil { + fmt.Printf("\tSerial Number:\t%s\n", bytesAsString(b)) + } + if b, err := wb.conn.ReadInputRegisters(solaxRegFirmwareVersion, 1); err == nil { + v := encoding.Uint16(b) + fmt.Printf("\tFirmware Version:\tV%d.%02d\n", v/100, v%100) + } + if b, err := wb.conn.ReadInputRegisters(solaxRegConnectionStrength, 1); err == nil { + fmt.Printf("\tConnection Strength (RSSI):\t%d%%\n", encoding.Uint16(b)) + } + if b, err := wb.conn.ReadInputRegisters(solaxRegFaultCode, 2); err == nil { + code := binary.BigEndian.Uint32(b) + fmt.Printf("\tFault Code:\t0x%08X", code) + + // Collect all set bits + var setBits []string + for bitIndex := range 32 { + if (code & (1 << bitIndex)) != 0 { // Check if the bit is set + setBits = append(setBits, fmt.Sprintf("%d", bitIndex+1)) // Add the 1-based bit number + } + } + if len(setBits) > 0 { + fmt.Printf(", Set Bits: %s\n", strings.Join(setBits, ",")) + } else { + fmt.Printf(", Set Bits: None\n") + } + } + if b, err := wb.conn.ReadInputRegisters(solaxRegLockState, 1); err == nil { + switch state := encoding.Uint16(b); state { + case 0: + fmt.Printf("\tLock State:\tUnlocked (%d)\n", state) + case 1: + fmt.Printf("\tLock State:\tLocked (%d)\n", state) + default: + fmt.Printf("\tLock State:\tUnknown (%d)\n", state) + } + } + if b, err := wb.conn.ReadHoldingRegisters(solaxRegDeviceMode, 1); err == nil { + switch state := encoding.Uint16(b); state { + case solaxModeStop: + fmt.Printf("\tDevice Mode:\tStop (%d)\n", state) + case solaxModeFast: + fmt.Printf("\tDevice Mode:\tFast (%d)\n", state) + case solaxModeECO: + fmt.Printf("\tDevice Mode:\tECO (%d)\n", state) + default: + fmt.Printf("\tDevice Mode:\tUnknown (%d)\n", state) + } + } +} diff --git a/charger/solax_decorators.go b/charger/solax_decorators.go new file mode 100644 index 000000000..afec72b13 --- /dev/null +++ b/charger/solax_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 decorateSolax(base *Solax, phaseSwitcher func(int) error, phaseGetter func() (int, error)) api.Charger { + switch { + case phaseSwitcher == nil: + return base + + case phaseGetter == nil && phaseSwitcher != nil: + return &struct { + *Solax + api.PhaseSwitcher + }{ + Solax: base, + PhaseSwitcher: &decorateSolaxPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + + case phaseGetter != nil && phaseSwitcher != nil: + return &struct { + *Solax + api.PhaseGetter + api.PhaseSwitcher + }{ + Solax: base, + PhaseGetter: &decorateSolaxPhaseGetterImpl{ + phaseGetter: phaseGetter, + }, + PhaseSwitcher: &decorateSolaxPhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decorateSolaxPhaseGetterImpl struct { + phaseGetter func() (int, error) +} + +func (impl *decorateSolaxPhaseGetterImpl) GetPhases() (int, error) { + return impl.phaseGetter() +} + +type decorateSolaxPhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateSolaxPhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} diff --git a/templates/definition/charger/solax-g2.yaml b/templates/definition/charger/solax-g2.yaml index c3d8dbd44..6cbdeb11c 100644 --- a/templates/definition/charger/solax-g2.yaml +++ b/templates/definition/charger/solax-g2.yaml @@ -3,12 +3,12 @@ products: - brand: Solax description: generic: X3-HAC -capabilities: ["mA"] +capabilities: ["1p3p", "mA"] requirements: evcc: ["sponsorship"] description: - de: Die Wallbox muss sich im Modus "Schnell" befinden und vom Wechselrichtersystem entkoppelt sein. - en: The charger must be in “Fast” mode and decoupled from the inverter system. + de: Die Wallbox muss sich im Modus "Schnell" befinden und vom Wechselrichtersystem entkoppelt sein. Die Wallbox muss Firmware Version V9.05 oder höher installiert haben, damit die Phasenumschaltung funktioniert. + en: The charger must be in “Fast” mode and decoupled from the inverter system. For the phase switching to work, the charger must have firmware version V9.05 or higher installed. params: - name: modbus choice: ["rs485"]