Add Sigenergy EVDC charger (#32023)

Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>
This commit is contained in:
lasseb13 2026-07-24 09:02:19 +02:00 • committed by GitHub
parent c8517b0237
commit cd9812ef14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 726 additions and 0 deletions

375
charger/sigenergy-evdc.go Normal file
View file

@ -0,0 +1,375 @@
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"
"fmt"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/volkszaehler/mbmd/encoding"
)
// SigenergyEVDC is the Sigenergy EVDC DC charger module attached to a
// SigenStor/Sigen Hybrid inverter. All registers live in the hybrid
// inverter's Modbus slave address space; the EVDC has no own slave ID.
type SigenergyEVDC struct {
conn *modbus.Connection
enabled bool
ratedPower uint32 // W
inputG func() ([]byte, error)
}
const (
// input registers (FC04), all on the hybrid inverter's slave ID
evdcBase = 31500 // start of the bulk-read block
evdcRegVehicleVoltage = 31500 // U16, 0.1 V
evdcRegChargingCurrent = 31501 // U16, 0.1 A
evdcRegOutputPower = 31502 // S32, W (assumed negative while discharging)
evdcRegVehicleSoc = 31504 // U16, 0.1 %
evdcRegSessionEnergy = 31505 // U32, 10 Wh, single session
evdcRegSessionDuration = 31507 // U32, s, single session
evdcRegRunningState = 31513 // U16
evdcRegDischargeCurrent = 31514 // U16, 0.1 A
evdcRegTotalEnergy = 31519 // U32, 10 Wh
evdcRegTotalDischargeEnergy = 31521 // U32, 10 Wh
evdcRegRatedPower = 31523 // U32, W
evdcRegRatedDischargePower = 31525 // U32, W
// 31515/31517 (session discharge energy/duration, U32) exist but are unused
// 31509/31511 inside the bulk-read block are inverter PV registers — not EVDC data
// holding registers
evdcRegStartStop = 41000 // U16, WO, single FC06 write only (41001 is reserved): 0 = start, 1 = stop
evdcRegPowerLimit = 41002 // U32, W, RW, FC10; clamped to [evdcMinPower, rated] — never write 0!
evdcRegDischargeLimit = 41004 // U32, W, RW; discharge control is out of scope
evdcInputLen = 23 // registers 31500-31522 in a single FC04 read
// minimum setpoint
evdcMinPower = 500 // 1A@500V per DC CCS Power Classes
evdcMinCurrent = 1.0 // A
// evcc current setpoints are converted using the 3-phase AC convention
evdcPowerPerAmp = 230 * 3 // W/A
)
// running states of evdcRegRunningState
const (
evdcStateIdle = 0x00
evdcStateOccupied = 0x01 // gun plugged in but not detected
evdcStatePreparing = 0x02 // establishing communication
evdcStateCharging = 0x03
evdcStateFault = 0x04
evdcStateScheduled = 0x05
evdcStateEnded = 0x06
evdcStateUnavailable = 0x07 // under maintenance
evdcStateDischarging = 0x08
evdcStateAlarm = 0x09
evdcStateInsulation = 0x0A // insulation detection in progress
)
var evdcStateNames = map[uint16]string{
evdcStateIdle: "Idle",
evdcStateOccupied: "Occupied",
evdcStatePreparing: "Preparing",
evdcStateCharging: "Charging",
evdcStateFault: "Fault",
evdcStateScheduled: "Scheduled",
evdcStateEnded: "Ended",
evdcStateUnavailable: "Unavailable",
evdcStateDischarging: "Discharging",
evdcStateAlarm: "Alarm",
evdcStateInsulation: "Insulation detection",
}
func init() {
registry.AddCtx("sigenergy-evdc", NewSigenergyEVDCFromConfig)
}
// NewSigenergyEVDCFromConfig creates a new Sigenergy EVDC ModbusTCP charger
func NewSigenergyEVDCFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
cc := modbus.TcpSettings{
ID: 1,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewSigenergyEVDC(ctx, cc.URI, cc.ID)
}
// NewSigenergyEVDC creates a new charger
func NewSigenergyEVDC(ctx context.Context, uri string, slaveID uint8) (*SigenergyEVDC, error) {
conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
if err != nil {
return nil, err
}
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
log := util.NewLogger("sigenergy-evdc")
conn.Logger(log.TRACE)
wb := newSigenergyEVDC(conn)
// probe rated power: verifies DC charger presence and V2.9 firmware, value clamps setpoints
b, err := conn.ReadInputRegisters(evdcRegRatedPower, 2)
if err != nil {
return nil, fmt.Errorf("no DC charger present or firmware too old (requires Modbus protocol V2.9): %w", err)
}
wb.ratedPower = encoding.Uint32(b)
// seed enabled state so evcc restarts mid-session report the true state
b, err = conn.ReadInputRegisters(evdcRegRunningState, 1)
if err != nil {
return nil, err
}
wb.enabled = encoding.Uint16(b) == evdcStateCharging
return wb, nil
}
// newSigenergyEVDC wires the struct without sponsor gate and device probe (also used by tests)
func newSigenergyEVDC(conn *modbus.Connection) *SigenergyEVDC {
wb := &SigenergyEVDC{
conn: conn,
}
// all cyclic values come from a single cached bulk read to respect the
// device's documented 1000ms minimum request period
wb.inputG = util.Cached(func() ([]byte, error) {
return wb.conn.ReadInputRegisters(evdcBase, evdcInputLen)
}, time.Second)
return wb
}
// evdcInput returns the bytes of the given register within the cached bulk read
func evdcInput(b []byte, reg uint16, n int) []byte {
off := 2 * int(reg-evdcBase)
return b[off : off+n]
}
// Status implements the api.Charger interface
func (wb *SigenergyEVDC) Status() (api.ChargeStatus, error) {
b, err := wb.inputG()
if err != nil {
return api.StatusNone, err
}
switch state := encoding.Uint16(evdcInput(b, evdcRegRunningState, 2)); state {
case evdcStateIdle:
return api.StatusA, nil
case evdcStateOccupied, evdcStatePreparing, evdcStateScheduled, evdcStateEnded, evdcStateInsulation:
return api.StatusB, nil
case evdcStateCharging:
wb.enabled = true
return api.StatusC, nil
case evdcStateDischarging:
return api.StatusC, nil
case evdcStateFault, evdcStateUnavailable, evdcStateAlarm:
return api.StatusNone, fmt.Errorf("device state: %s", evdcStateNames[state])
default:
return api.StatusNone, fmt.Errorf("invalid status: %d", state)
}
}
// Enabled implements the api.Charger interface
func (wb *SigenergyEVDC) Enabled() (bool, error) {
return wb.enabled, nil
}
// Enable implements the api.Charger interface
func (wb *SigenergyEVDC) Enable(enable bool) error {
var v uint16
if !enable {
v = 1
}
// single FC06 write only — 41001 is a reserved register
_, err := wb.conn.WriteSingleRegister(evdcRegStartStop, v)
if err == nil {
wb.enabled = enable
}
return err
}
// MaxCurrent implements the api.Charger interface
func (wb *SigenergyEVDC) MaxCurrent(current int64) error {
return wb.MaxCurrentMillis(float64(current))
}
var _ api.ChargerEx = (*SigenergyEVDC)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (wb *SigenergyEVDC) MaxCurrentMillis(current float64) error {
if current < evdcMinCurrent {
return fmt.Errorf("invalid current %.3g", current)
}
power := min(max(uint32(current*evdcPowerPerAmp), evdcMinPower), wb.ratedPower)
b := make([]byte, 4)
encoding.PutUint32(b, power)
_, err := wb.conn.WriteMultipleRegisters(evdcRegPowerLimit, 2, b)
return err
}
var _ api.CurrentLimiter = (*SigenergyEVDC)(nil)
// GetMinMaxCurrent implements the api.CurrentLimiter interface
func (wb *SigenergyEVDC) GetMinMaxCurrent() (float64, float64, error) {
return evdcMinCurrent, float64(wb.ratedPower) / evdcPowerPerAmp, nil
}
var _ api.Meter = (*SigenergyEVDC)(nil)
// CurrentPower implements the api.Meter interface
func (wb *SigenergyEVDC) CurrentPower() (float64, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
// S32, negative while discharging (to be confirmed on hardware, see spec)
return float64(encoding.Int32(evdcInput(b, evdcRegOutputPower, 4))), nil
}
var _ api.MeterEnergy = (*SigenergyEVDC)(nil)
// TotalEnergy implements the api.MeterEnergy interface
func (wb *SigenergyEVDC) TotalEnergy() (float64, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Uint32(evdcInput(b, evdcRegTotalEnergy, 4))) / 100, nil
}
var _ api.MeterReturnEnergy = (*SigenergyEVDC)(nil)
// ReturnEnergy implements the api.MeterReturnEnergy interface
func (wb *SigenergyEVDC) ReturnEnergy() (float64, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Uint32(evdcInput(b, evdcRegTotalDischargeEnergy, 4))) / 100, nil
}
var _ api.ChargeRater = (*SigenergyEVDC)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (wb *SigenergyEVDC) ChargedEnergy() (float64, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Uint32(evdcInput(b, evdcRegSessionEnergy, 4))) / 100, nil
}
var _ api.ChargeTimer = (*SigenergyEVDC)(nil)
// ChargeDuration implements the api.ChargeTimer interface
func (wb *SigenergyEVDC) ChargeDuration() (time.Duration, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
return time.Duration(encoding.Uint32(evdcInput(b, evdcRegSessionDuration, 4))) * time.Second, nil
}
var _ api.Battery = (*SigenergyEVDC)(nil)
// Soc implements the api.Battery interface
func (wb *SigenergyEVDC) Soc() (float64, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
if soc := float64(encoding.Uint16(evdcInput(b, evdcRegVehicleSoc, 2))) / 10; soc > 0 {
return soc, nil
}
// no vehicle connected or no BMS data
return 0, api.ErrNotAvailable
}
var _ api.Diagnosis = (*SigenergyEVDC)(nil)
// Diagnose implements the api.Diagnosis interface
func (wb *SigenergyEVDC) Diagnose() {
if b, err := wb.conn.ReadInputRegisters(evdcRegRunningState, 1); err == nil {
state := encoding.Uint16(b)
name := evdcStateNames[state]
if name == "" {
name = "Unknown"
}
fmt.Printf("\tRunning state:\t%d (%s)\n", state, name)
}
if b, err := wb.conn.ReadInputRegisters(evdcRegVehicleVoltage, 1); err == nil {
fmt.Printf("\tVehicle voltage:\t%.1f V\n", float64(encoding.Uint16(b))/10)
}
if b, err := wb.conn.ReadInputRegisters(evdcRegChargingCurrent, 1); err == nil {
fmt.Printf("\tCharging current:\t%.1f A\n", float64(encoding.Uint16(b))/10)
}
if b, err := wb.conn.ReadInputRegisters(evdcRegDischargeCurrent, 1); err == nil {
fmt.Printf("\tDischarging current:\t%.1f A\n", float64(encoding.Uint16(b))/10)
}
if b, err := wb.conn.ReadInputRegisters(evdcRegTotalDischargeEnergy, 2); err == nil {
fmt.Printf("\tTotal discharged:\t%.2f kWh\n", float64(encoding.Uint32(b))/100)
}
fmt.Printf("\tRated power:\t%d W\n", wb.ratedPower)
if b, err := wb.conn.ReadInputRegisters(evdcRegRatedDischargePower, 2); err == nil {
fmt.Printf("\tRated discharge power:\t%d W\n", encoding.Uint32(b))
}
// power limit readback: FC04 per spec text, FC03 field-proven fallback
if b, err := wb.conn.ReadInputRegisters(evdcRegPowerLimit, 2); err == nil {
fmt.Printf("\tPower limit:\t%d W\n", encoding.Uint32(b))
} else if b, err := wb.conn.ReadHoldingRegisters(evdcRegPowerLimit, 2); err == nil {
fmt.Printf("\tPower limit (FC03):\t%d W\n", encoding.Uint32(b))
}
if b, err := wb.conn.ReadInputRegisters(evdcRegDischargeLimit, 2); err == nil {
fmt.Printf("\tDischarge limit:\t%d W\n", encoding.Uint32(b))
}
}
var _ api.Resurrector = (*SigenergyEVDC)(nil)
// WakeUp implements the api.Resurrector interface
func (wb *SigenergyEVDC) WakeUp() error {
// re-issue Start: recovers sessions stopped on the vehicle/vendor-app side,
// which latch until a new start command (device-verified)
_, err := wb.conn.WriteSingleRegister(evdcRegStartStop, 0)
return err
}

View file

@ -0,0 +1,333 @@
package charger
import (
"context"
"net"
"sync"
"testing"
"time"
"github.com/andig/mbserver"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util/modbus"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type evdcWrite struct {
funcCode uint8
addr uint16
args []uint16
}
// evdcHandler mocks the hybrid inverter's EVDC register space
type evdcHandler struct {
mbserver.RequestHandler
regs map[uint16]uint16
writes []evdcWrite
failWrites bool
}
func (h *evdcHandler) HandleInputRegisters(req *mbserver.InputRegistersRequest) ([]uint16, error) {
res := make([]uint16, 0, req.Quantity)
for i := range req.Quantity {
v, ok := h.regs[req.Addr+i]
if !ok {
return nil, mbserver.ErrIllegalDataAddress
}
res = append(res, v)
}
return res, nil
}
func (h *evdcHandler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) ([]uint16, error) {
if !req.IsWrite {
return nil, mbserver.ErrIllegalDataAddress
}
if h.failWrites {
return nil, mbserver.ErrIllegalDataValue
}
h.writes = append(h.writes, evdcWrite{req.WriteFuncCode, req.Addr, req.Args})
return req.Args, nil
}
// evdcRegs returns a fully populated 31500-31520 block with the given running state
func evdcRegs(state uint16) map[uint16]uint16 {
regs := make(map[uint16]uint16)
for reg := uint16(evdcBase); reg < evdcBase+evdcInputLen; reg++ {
regs[reg] = 0
}
regs[evdcRegRunningState] = state
return regs
}
// shared mock server: mbserver.Stop() races its accept goroutine (unlocked
// tcpListener read vs. nil assignment), so the server is started once and
// never stopped; handler state is reset per test
var (
evdcOnce sync.Once
evdcURI string
evdcSrvH = &evdcHandler{RequestHandler: new(mbserver.DummyHandler)}
)
// evdcTestCharger connects a charger to the shared mock Modbus server
func evdcTestCharger(t *testing.T, regs map[uint16]uint16) (*SigenergyEVDC, *evdcHandler) {
t.Helper()
evdcOnce.Do(func() {
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
srv, err := mbserver.New(evdcSrvH)
require.NoError(t, err)
require.NoError(t, srv.Start(l))
evdcURI = l.Addr().String()
})
evdcSrvH.regs = regs
evdcSrvH.writes = nil
evdcSrvH.failWrites = false
conn, err := modbus.NewConnection(context.Background(), evdcURI, "", "", 0, modbus.Tcp, 1)
require.NoError(t, err)
wb := newSigenergyEVDC(conn)
wb.ratedPower = 25000
return wb, evdcSrvH
}
func TestSigenergyEVDCStatus(t *testing.T) {
tc := []struct {
state uint16
status api.ChargeStatus
err bool
}{
{evdcStateIdle, api.StatusA, false},
{evdcStateOccupied, api.StatusB, false},
{evdcStatePreparing, api.StatusB, false},
{evdcStateCharging, api.StatusC, false},
{evdcStateScheduled, api.StatusB, false},
{evdcStateEnded, api.StatusB, false},
{evdcStateInsulation, api.StatusB, false},
{evdcStateDischarging, api.StatusC, false},
{evdcStateFault, api.StatusNone, true},
{evdcStateUnavailable, api.StatusNone, true},
{evdcStateAlarm, api.StatusNone, true},
{0x0B, api.StatusNone, true},
}
for _, tc := range tc {
wb, _ := evdcTestCharger(t, evdcRegs(tc.state))
status, err := wb.Status()
if tc.err {
assert.Error(t, err, "state %d", tc.state)
} else {
assert.NoError(t, err, "state %d", tc.state)
}
assert.Equal(t, tc.status, status, "state %d", tc.state)
}
}
func TestSigenergyEVDCMeasurements(t *testing.T) {
regs := evdcRegs(evdcStateCharging)
regs[evdcRegOutputPower] = 0
regs[evdcRegOutputPower+1] = 11500 // 11500 W
regs[evdcRegVehicleSoc] = 655 // 65.5 %
regs[evdcRegSessionEnergy] = 0
regs[evdcRegSessionEnergy+1] = 1234 // 12.34 kWh
regs[evdcRegSessionDuration] = 0
regs[evdcRegSessionDuration+1] = 3600 // 1h
regs[evdcRegTotalEnergy] = 1
regs[evdcRegTotalEnergy+1] = 34464 // 65536+34464 = 100000 -> 1000.00 kWh
regs[evdcRegTotalDischargeEnergy] = 0
regs[evdcRegTotalDischargeEnergy+1] = 500 // 5.00 kWh
wb, _ := evdcTestCharger(t, regs)
power, err := wb.CurrentPower()
require.NoError(t, err)
assert.Equal(t, 11500.0, power)
soc, err := wb.Soc()
require.NoError(t, err)
assert.Equal(t, 65.5, soc)
charged, err := wb.ChargedEnergy()
require.NoError(t, err)
assert.Equal(t, 12.34, charged)
dur, err := wb.ChargeDuration()
require.NoError(t, err)
assert.Equal(t, time.Hour, dur)
total, err := wb.TotalEnergy()
require.NoError(t, err)
assert.Equal(t, 1000.0, total)
returned, err := wb.ReturnEnergy()
require.NoError(t, err)
assert.Equal(t, 5.0, returned)
}
func TestSigenergyEVDCNegativePower(t *testing.T) {
regs := evdcRegs(evdcStateDischarging)
regs[evdcRegOutputPower] = 0xFFFF
regs[evdcRegOutputPower+1] = 0xEC78 // -5000 W as S32
wb, _ := evdcTestCharger(t, regs)
power, err := wb.CurrentPower()
require.NoError(t, err)
assert.Equal(t, -5000.0, power)
}
func TestSigenergyEVDCSocNotAvailable(t *testing.T) {
wb, _ := evdcTestCharger(t, evdcRegs(evdcStateIdle)) // all-zero block
_, err := wb.Soc()
assert.ErrorIs(t, err, api.ErrNotAvailable)
}
func TestSigenergyEVDCEnable(t *testing.T) {
wb, h := evdcTestCharger(t, evdcRegs(evdcStateOccupied))
require.NoError(t, wb.Enable(true))
enabled, err := wb.Enabled()
require.NoError(t, err)
assert.True(t, enabled)
require.NoError(t, wb.Enable(false))
enabled, err = wb.Enabled()
require.NoError(t, err)
assert.False(t, enabled)
// start/stop is a single-register FC06 write to 41000: 0=start, 1=stop
require.Len(t, h.writes, 2)
assert.Equal(t, evdcWrite{6, evdcRegStartStop, []uint16{0}}, h.writes[0])
assert.Equal(t, evdcWrite{6, evdcRegStartStop, []uint16{1}}, h.writes[1])
}
func TestSigenergyEVDCEnableWriteFailure(t *testing.T) {
wb, h := evdcTestCharger(t, evdcRegs(evdcStateOccupied))
h.failWrites = true
assert.Error(t, wb.Enable(true))
// failed write must not flip the cached state
enabled, err := wb.Enabled()
require.NoError(t, err)
assert.False(t, enabled)
}
func TestSigenergyEVDCEnabledSync(t *testing.T) {
// charging syncs the cache to enabled
wb, _ := evdcTestCharger(t, evdcRegs(evdcStateCharging))
_, err := wb.Status()
require.NoError(t, err)
enabled, _ := wb.Enabled()
assert.True(t, enabled)
// ambiguous lifecycle state leaves the cache untouched
wb, _ = evdcTestCharger(t, evdcRegs(evdcStateEnded))
wb.enabled = true
_, err = wb.Status()
require.NoError(t, err)
enabled, _ = wb.Enabled()
assert.True(t, enabled)
// discharging must NOT sync (vendor-initiated V2X, evcc-disabled loadpoint stays consistent)
wb, _ = evdcTestCharger(t, evdcRegs(evdcStateDischarging))
_, err = wb.Status()
require.NoError(t, err)
enabled, _ = wb.Enabled()
assert.False(t, enabled)
}
func TestSigenergyEVDCMaxCurrent(t *testing.T) {
tc := []struct {
current float64
power uint16 // expected low word; high word is 0 in all cases
}{
{6, 4140},
{16, 11040},
{1.45, 1000}, // 1000.5 W truncated to 1000
{1.0, 690}, // minimum current -> 690 W
{40, 25000}, // 27600 W clamped down to rated power
}
for _, tc := range tc {
wb, h := evdcTestCharger(t, evdcRegs(evdcStateCharging))
require.NoError(t, wb.MaxCurrentMillis(tc.current))
require.Len(t, h.writes, 1, "current %v", tc.current)
w := h.writes[0]
assert.Equal(t, uint8(16), w.funcCode, "current %v", tc.current)
assert.Equal(t, uint16(evdcRegPowerLimit), w.addr, "current %v", tc.current)
assert.Equal(t, []uint16{0, tc.power}, w.args, "current %v", tc.current)
}
}
func TestSigenergyEVDCMaxCurrentInvalid(t *testing.T) {
wb, h := evdcTestCharger(t, evdcRegs(evdcStateCharging))
assert.Error(t, wb.MaxCurrentMillis(0.9))
assert.Error(t, wb.MaxCurrentMillis(0))
assert.Error(t, wb.MaxCurrentMillis(-1))
assert.Empty(t, h.writes)
}
func TestSigenergyEVDCMinMaxCurrent(t *testing.T) {
wb, _ := evdcTestCharger(t, evdcRegs(evdcStateIdle))
minA, maxA, err := wb.GetMinMaxCurrent()
require.NoError(t, err)
assert.InDelta(t, 1.0, minA, 0.001) // evdcMinCurrent
assert.InDelta(t, 36.23, maxA, 0.01) // 25000 W / 690
}
func TestSigenergyEVDCSponsorGate(t *testing.T) {
// go-e tests set the global sponsor.Subject and never reset it
old := sponsor.Subject
sponsor.Subject = ""
t.Cleanup(func() { sponsor.Subject = old })
// tests run without sponsorship: the public constructor must refuse
_, err := NewSigenergyEVDC(t.Context(), "localhost:0", 1)
assert.ErrorIs(t, err, api.ErrSponsorRequired)
}
func TestSigenergyEVDCReadFailure(t *testing.T) {
// missing register in the bulk-read block -> IllegalDataAddress propagates
regs := evdcRegs(evdcStateCharging)
delete(regs, uint16(evdcRegRunningState))
wb, _ := evdcTestCharger(t, regs)
_, err := wb.Status()
assert.Error(t, err)
_, err = wb.CurrentPower()
assert.Error(t, err)
}
func TestSigenergyEVDCWakeUp(t *testing.T) {
wb, h := evdcTestCharger(t, evdcRegs(evdcStateOccupied))
// realistic scenario: cache latched true from a prior session, external stop happened
wb.enabled = true
require.NoError(t, wb.WakeUp())
// single FC06 start write, must not touch the enabled cache
require.Len(t, h.writes, 1)
assert.Equal(t, evdcWrite{6, evdcRegStartStop, []uint16{0}}, h.writes[0])
enabled, err := wb.Enabled()
require.NoError(t, err)
assert.True(t, enabled)
}

View file

@ -0,0 +1,18 @@
template: sigenergy-evdc
products:
- brand: Sigenergy
description:
generic: EVDC
capabilities: ["mA", "meter", "dim"]
requirements:
description:
de: Modbus TCP muss in der Konfigurations-App mit Installateursrechten aktiviert werden. Die Modbus-ID ist die des Hybrid-Wechselrichters, an dem der EVDC angeschlossen ist. Benötigt aktuelle Firmware (Modbus-Protokoll V2.9).
en: Modbus TCP must be enabled in the configuration app with installer rights. The Modbus ID is that of the hybrid inverter the EVDC is attached to. Requires current firmware (Modbus protocol V2.9).
evcc: ["sponsorship"]
params:
- name: modbus
choice: ["tcpip"]
id: 1
render: |
type: sigenergy-evdc
{{- include "modbus" . }}