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)
}