From 16812adcb2add5c1478560f3f9aef26031181913 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:42:15 +0100 Subject: [PATCH] Add RAEDIAN NEO and NEX AC charger (#28053) --- charger/raedian.go | 245 ++++++++++++++++++++++ templates/definition/charger/raedian.yaml | 16 ++ 2 files changed, 261 insertions(+) create mode 100644 charger/raedian.go create mode 100644 templates/definition/charger/raedian.yaml diff --git a/charger/raedian.go b/charger/raedian.go new file mode 100644 index 000000000..e9967c924 --- /dev/null +++ b/charger/raedian.go @@ -0,0 +1,245 @@ +package charger + +// LICENSE + +// Copyright (c) evcc.io (andig, naltatis, premultiply) + +// This module is NOT covered by the MIT license. All rights reserved. + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" +) + +// https://api.library.loxone.com/downloader/file/2425/Modbus%20Protocol%20RAEDIAN%20AC%20Wallbox%20v0.3.pdf + +// Raedian charger implementation +type Raedian struct { + log *util.Logger + conn *modbus.Connection + curr uint32 // mA +} + +const ( + raedianRegStatus = 0x800C // uint32 RO ENUM + raedianRegCurrents = 0x8010 // uint32 RO mA + raedianRegVoltages = 0x8016 // uint32 RO 0.1V + raedianRegPower = 0x801C // uint32 RO W + raedianRegChargedEnergy = 0x801E // uint32 RO Wh + raedianRegMaxCurrent = 0x8100 // uint32 WR mA + raedianRegPhases = 0x8102 // uint16 WO +) + +func init() { + registry.AddCtx("raedian", NewRaedianFromConfig) +} + +// NewRaedianFromConfig creates a Raedian charger from generic config +func NewRaedianFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { + cc := modbus.Settings{ + ID: 1, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewRaedian(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID) +} + +// NewRaedian creates Raedian charger +func NewRaedian(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (*Raedian, error) { + conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID) + if err != nil { + return nil, err + } + + log := util.NewLogger("raedian") + conn.Logger(log.TRACE) + + wb := &Raedian{ + log: log, + conn: conn, + curr: 6000, // assume min current + } + + if b, err := wb.conn.ReadHoldingRegisters(raedianRegMaxCurrent, 2); err == nil { + if cur := binary.BigEndian.Uint32(b); cur >= 6000 { + wb.curr = cur + } + } + + return wb, nil +} + +// Status implements the api.Charger interface +func (wb *Raedian) Status() (api.ChargeStatus, error) { + b, err := wb.conn.ReadHoldingRegisters(raedianRegStatus, 2) + if err != nil { + return api.StatusNone, err + } + + // Register layout: A3 A2 A1 A0 (big-endian) + // A1 (b[2]): Bit7 = current limit flag, Bits 6~0 = charging state + state := b[2] & 0x7F + switch state { + case 0x00: + return api.StatusA, nil + case 0x01, 0x02: + return api.StatusB, nil + case 0x03, 0x04: + return api.StatusC, nil + default: + return api.StatusNone, fmt.Errorf("invalid status: %#x", state) + } +} + +var _ api.StatusReasoner = (*Raedian)(nil) + +// StatusReason implements the api.StatusReasoner interface +func (wb *Raedian) StatusReason() (api.Reason, error) { + b, err := wb.conn.ReadHoldingRegisters(raedianRegStatus, 2) + if err != nil { + return api.ReasonUnknown, err + } + + // A1 (b[2]) Bits 6~0: 0x01 = State B1, pending authorization + if b[2]&0x7F == 0x01 { + return api.ReasonWaitingForAuthorization, nil + } + + return api.ReasonUnknown, nil +} + +// Enabled implements the api.Charger interface +func (wb *Raedian) Enabled() (bool, error) { + b, err := wb.conn.ReadHoldingRegisters(raedianRegMaxCurrent, 2) + if err != nil { + return false, err + } + + cur := binary.BigEndian.Uint32(b) + if cur != 0 { + wb.curr = cur + } + + return cur != 0, nil +} + +// Enable implements the api.Charger interface +func (wb *Raedian) Enable(enable bool) error { + var cur uint32 + if enable { + cur = wb.curr + } + + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, cur) + + _, err := wb.conn.WriteMultipleRegisters(raedianRegMaxCurrent, 2, b) + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *Raedian) MaxCurrent(current int64) error { + return wb.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*Raedian)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (wb *Raedian) MaxCurrentMillis(current float64) error { + curr := uint32(current * 1000) // convert A to mA + + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, curr) + + _, err := wb.conn.WriteMultipleRegisters(raedianRegMaxCurrent, 2, b) + if err == nil { + wb.curr = curr + } + + return err +} + +var _ api.Meter = (*Raedian)(nil) + +// CurrentPower implements the api.Meter interface +func (wb *Raedian) CurrentPower() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(raedianRegPower, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)), nil +} + +var _ api.ChargeRater = (*Raedian)(nil) + +// ChargedEnergy implements the api.ChargeRater interface +func (wb *Raedian) ChargedEnergy() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(raedianRegChargedEnergy, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)) / 1000, nil +} + +// getPhaseValues reads 3 sequential uint32 registers in a single Modbus operation +func (wb *Raedian) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) { + b, err := wb.conn.ReadHoldingRegisters(reg, 6) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / divider + } + + return res[0], res[1], res[2], nil +} + +var _ api.PhaseCurrents = (*Raedian)(nil) + +// Currents implements the api.PhaseCurrents interface +func (wb *Raedian) Currents() (float64, float64, float64, error) { + return wb.getPhaseValues(raedianRegCurrents, 1e3) +} + +var _ api.PhaseVoltages = (*Raedian)(nil) + +// Voltages implements the api.PhaseVoltages interface +func (wb *Raedian) Voltages() (float64, float64, float64, error) { + return wb.getPhaseValues(raedianRegVoltages, 10) +} + +var _ api.PhaseSwitcher = (*Raedian)(nil) + +// Phases1p3p implements the api.PhaseSwitcher interface +func (wb *Raedian) Phases1p3p(phases int) error { + u := uint16(0x01) + if phases == 3 { + u = 0x02 + } + + _, err := wb.conn.WriteSingleRegister(raedianRegPhases, u) + return err +} diff --git a/templates/definition/charger/raedian.yaml b/templates/definition/charger/raedian.yaml new file mode 100644 index 000000000..6d526e818 --- /dev/null +++ b/templates/definition/charger/raedian.yaml @@ -0,0 +1,16 @@ +template: raedian +products: + - brand: RAEDIAN + description: + generic: NEO AC Wallbox + - brand: RAEDIAN + description: + generic: NEX AC Wallbox +capabilities: ["1p3p", "mA"] +params: + - name: modbus + choice: ["rs485", "tcpip"] + id: 1 +render: | + type: raedian + {{- include "modbus" . }}