Alpitronic HYC: fix status mapping and bundle register reads (#32260)

This commit is contained in:
premultiply 2026-08-02 09:50:34 +02:00 • committed by GitHub
parent 63b7e06612
commit c32ec2e325
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 528 additions and 52 deletions

View file

@ -20,6 +20,7 @@ package charger
import (
"context"
"encoding/hex"
"errors"
"fmt"
"time"
@ -32,25 +33,82 @@ import (
// AlpitronicHYC charger implementation
type AlpitronicHYC struct {
log *util.Logger
conn *modbus.Connection
inputG func() ([]byte, error)
curr float64
connector uint16
}
// input registers of the charging station (connector 0)
//
//nolint:unused // complete register map as documented by the vendor
const (
// Input
hycRegState = 0
hycRegChargingPower = 4
hycRegChargeTime = 6
hycRegChargedEnergy = 7
hycRegSoC = 8
hycRegVID = 18
hycRegIdTag = 22
hycRegTotalChargedEnergy = 32
hycRegStationTime = 0 // UINT32, s
hycRegStationNumConnectors = 2 // UINT16
hycRegStationState = 3 // UINT16, 0-Available, 8-Unavailable, 10-Faulted
hycRegStationPowerDrained = 4 // UINT32, W
hycRegStationSerial = 6 // 24 byte string
hycRegStationLoadManagement = 18 // UINT16, bool
hycRegStationChargepointId = 30 // 32 byte string
hycRegStationVersionMajor = 46 // UINT16
hycRegStationVersionMinor = 47 // UINT16
hycRegStationVersionPatch = 48 // UINT16
hycRegStationVarInductive = 49 // UINT32, var
hycRegStationVarCapacitive = 51 // UINT32, var
)
// Holding
hycRegMaxPowerAC = 0
// input registers, relative to the connector block (1xx..4xx)
const (
hycRegState = 0 // UINT16
hycRegChargingVoltage = 1 // UINT32, cV
hycRegChargingCurrent = 3 // UINT16, cA
hycRegChargingPower = 4 // UINT32, W
hycRegChargeTime = 6 // UINT16, s
hycRegChargedEnergy = 7 // UINT16, kWh/100
hycRegSoC = 8 // UINT16, %/100
hycRegConnectorType = 9 // UINT16, 0-ChargePoint, 1-CCS2, 2-CCS1, 3-CHAdeMO, 4-CCS_AC, 5-GBT, 6-MCS, 7-NACS
hycRegMaxChargingPowerDC = 10 // UINT32, W- includes the limit written by us
hycRegMinChargingPowerDC = 12 // UINT32, W
hycRegVarInductive = 14 // UINT32, var- deprecated, use hycRegStationVarInductive
hycRegVarCapacitive = 16 // UINT32, var- deprecated, use hycRegStationVarCapacitive
hycRegVID = 18 // 8 bytes
hycRegIdTag = 22 // 20 bytes
hycRegTotalChargedEnergy = 32 // INT64, Wh
hycRegMaxChargingPowerAC = 36 // UINT32, W- includes the limit written by us
// the connector block is gapless- read up to the last register in use (x32..x35)
hycInputLength = 36
)
// holding registers, relative to the connector block (0xx is the station)
const (
hycRegMaxPowerAC = 0 // UINT32, W per connector, VA for the station
hycRegSetReactivePower = 2 // INT32, var
)
// connector states as per register x00
const (
hycStateAvailable uint16 = iota
hycStatePreparingTagIdReady
hycStatePreparingEVReady
hycStateCharging
hycStateSuspendedEV
hycStateSuspendedEVSE
hycStateFinishing
hycStateReserved
hycStateUnavailable
hycStateUnavailableFwUpdate
hycStateFaulted
hycStateUnavailableConnObj
)
// a zero power limit makes the station report the connector as unavailable and
// abort a running session, so charging is inhibited with a limit that is too low
// for any connector to actually charge- see hycRegMinChargingPowerDC
const (
hycPowerPerAmp = 230 * 3 // W/A on the AC side
hycMinPowerAC = 1547 // W
hycMinCurrent = 2.25 // A, lowest current exceeding hycMinPowerAC
)
func init() {
@ -90,19 +148,54 @@ func NewAlpitronicHYC(ctx context.Context, settings modbus.TcpSettings, connecto
log := util.NewLogger("alpitronic")
conn.Logger(log.TRACE)
return newAlpitronicHYC(conn, connector)
}
// newAlpitronicHYC wires the struct without sponsor gate (also used by tests)
func newAlpitronicHYC(conn *modbus.Connection, connector uint16) (*AlpitronicHYC, error) {
wb := &AlpitronicHYC{
log: log,
conn: conn,
curr: 6,
curr: hycMinCurrent,
connector: connector,
}
// share a single bulk read of the connector's input block across all decoders.
// the station applies a fallback when it is not polled within GridFallbackTimeout,
// so keeping the number of roundtrips low matters here.
wb.inputG = util.Cached(func() ([]byte, error) {
return wb.conn.ReadInputRegisters(wb.reg(hycRegState), hycInputLength)
}, time.Second)
// seed the current limit from the charger- this also validates the connector
b, err := wb.conn.ReadHoldingRegisters(wb.reg(hycRegMaxPowerAC), 2)
if err != nil {
return nil, err
}
if power := encoding.Uint32(b); power > hycMinPowerAC {
wb.curr = float64(power) / hycPowerPerAmp
}
return wb, nil
}
// setCurrent writes the current limit as power
func (wb *AlpitronicHYC) setCurrent(current float64) error {
power := uint32(current * 230 * 3)
// hycInput returns the bytes of the given register within the cached input block
func hycInput(b []byte, reg uint16, n int) []byte {
off := 2 * int(reg)
return b[off : off+n]
}
// hycPower converts a charging current into the AC side power limit
func hycPower(current float64) uint32 {
if current <= 0 {
return 0
}
return uint32(current * hycPowerPerAmp)
}
// setPower writes the connector's power limit
func (wb *AlpitronicHYC) setPower(power uint32) error {
b := make([]byte, 4)
encoding.PutUint32(b, power)
@ -115,20 +208,42 @@ func (wb *AlpitronicHYC) reg(reg uint16) uint16 {
return (wb.connector * 100) + reg
}
// state returns the connector state
func (wb *AlpitronicHYC) state() (uint16, error) {
b, err := wb.inputG()
if err != nil {
return 0, err
}
return encoding.Uint16(hycInput(b, hycRegState, 2)), nil
}
// Status implements the api.Charger interface
func (wb *AlpitronicHYC) Status() (api.ChargeStatus, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegState), 1)
s, err := wb.state()
if err != nil {
return api.StatusNone, err
}
switch s := encoding.Uint16(b); s {
case 0, 7, 8, 9, 10:
switch s {
case
hycStateAvailable,
hycStatePreparingTagIdReady, // authorized, waiting for the cable to be plugged in
hycStateReserved,
hycStateUnavailable,
hycStateUnavailableFwUpdate,
hycStateUnavailableConnObj:
return api.StatusA, nil
case 1, 2, 4, 5, 6:
case
hycStatePreparingEVReady, // plugged in, waiting for authorization
hycStateSuspendedEV,
hycStateSuspendedEVSE,
hycStateFinishing:
return api.StatusB, nil
case 3:
case hycStateCharging:
return api.StatusC, nil
case hycStateFaulted:
return api.StatusNone, errors.New("connector state: faulted")
default:
return api.StatusNone, fmt.Errorf("invalid status: %d", s)
}
@ -141,17 +256,17 @@ func (wb *AlpitronicHYC) Enabled() (bool, error) {
return false, err
}
return encoding.Uint32(b) != 0, nil
return encoding.Uint32(b) > hycMinPowerAC, nil
}
// Enable implements the api.Charger interface
func (wb *AlpitronicHYC) Enable(enable bool) error {
var c float64
power := uint32(hycMinPowerAC)
if enable {
c = wb.curr
power = hycPower(wb.curr)
}
return wb.setCurrent(c)
return wb.setPower(power)
}
// MaxCurrent implements the api.Charger interface
@ -162,12 +277,16 @@ func (wb *AlpitronicHYC) MaxCurrent(current int64) error {
var _ api.ChargerEx = (*AlpitronicHYC)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
//
// api.CurrentLimiter is deliberately not implemented: registers x10/x36 contain
// the connector power limit written by evcc itself, so using them as upper bound
// would feed back into the limit. Use the loadpoint's maxcurrent instead.
func (wb *AlpitronicHYC) MaxCurrentMillis(current float64) error {
if current < 6 {
if current < hycMinCurrent {
return fmt.Errorf("invalid current %.1f", current)
}
err := wb.setCurrent(current)
err := wb.setPower(hycPower(current))
if err == nil {
wb.curr = current
}
@ -179,63 +298,63 @@ var _ api.Meter = (*AlpitronicHYC)(nil)
// CurrentPower implements the api.Meter interface
func (wb *AlpitronicHYC) CurrentPower() (float64, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegChargingPower), 2)
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Uint32(b)), err
return float64(encoding.Uint32(hycInput(b, hycRegChargingPower, 4))), nil
}
var _ api.ChargeTimer = (*AlpitronicHYC)(nil)
// ChargeDuration implements the api.ChargeTimer interface
func (wb *AlpitronicHYC) ChargeDuration() (time.Duration, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegChargeTime), 1)
b, err := wb.inputG()
if err != nil {
return 0, err
}
return time.Duration(encoding.Uint16(b)) * time.Second, nil
return time.Duration(encoding.Uint16(hycInput(b, hycRegChargeTime, 2))) * time.Second, nil
}
var _ api.ChargeRater = (*AlpitronicHYC)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (wb *AlpitronicHYC) ChargedEnergy() (float64, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegChargedEnergy), 1)
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Uint16(b)) / 100, err
return float64(encoding.Uint16(hycInput(b, hycRegChargedEnergy, 2))) / 100, nil
}
var _ api.MeterEnergy = (*AlpitronicHYC)(nil)
// TotalEnergy implements the api.MeterEnergy interface
func (wb *AlpitronicHYC) TotalEnergy() (float64, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegTotalChargedEnergy), 4)
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Int64(b)) / 1e3, err
return float64(encoding.Int64(hycInput(b, hycRegTotalChargedEnergy, 8))) / 1e3, nil
}
var _ api.StatusReasoner = (*AlpitronicHYC)(nil)
// StatusReason implements the api.StatusReasoner interface
func (wb *AlpitronicHYC) StatusReason() (api.Reason, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegState), 1)
s, err := wb.state()
if err != nil {
return api.ReasonUnknown, err
}
switch s := encoding.Uint16(b); s {
case 1:
switch s {
case hycStatePreparingEVReady:
return api.ReasonWaitingForAuthorization, nil
case 6:
case hycStateFinishing:
return api.ReasonDisconnectRequired, nil
default:
return api.ReasonUnknown, nil
@ -246,22 +365,17 @@ var _ api.Identifier = (*AlpitronicHYC)(nil)
// Identify implements the api.Identifier interface
func (wb *AlpitronicHYC) Identify() (string, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegVID), 4)
b, err := wb.inputG()
if err != nil {
return "", err
}
if !allZero(b) {
return hex.EncodeToString(b), nil
if vid := hycInput(b, hycRegVID, 8); !allZero(vid) {
return hex.EncodeToString(vid), nil
}
b, err = wb.conn.ReadInputRegisters(wb.reg(hycRegIdTag), 10)
if err != nil {
return "", err
}
if !allZero(b) {
return hex.EncodeToString(b), nil
if idTag := hycInput(b, hycRegIdTag, 20); !allZero(idTag) {
return hex.EncodeToString(idTag), nil
}
return "", nil
@ -271,12 +385,12 @@ var _ api.Battery = (*AlpitronicHYC)(nil)
// Soc implements the api.Battery interface
func (wb *AlpitronicHYC) Soc() (float64, error) {
b, err := wb.conn.ReadInputRegisters(wb.reg(hycRegSoC), 1)
b, err := wb.inputG()
if err != nil {
return 0, err
}
return float64(encoding.Uint16(b)) / 100, nil
return float64(encoding.Uint16(hycInput(b, hycRegSoC, 2))) / 100, nil
}
func allZero(s []byte) bool {

362
charger/alpitronic_test.go Normal file
View file

@ -0,0 +1,362 @@
package charger
import (
"context"
"net"
"strings"
"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 hycWrite struct {
funcCode uint8
addr uint16
args []uint16
}
// hycHandler mocks the Hypercharger's register space
type hycHandler struct {
mbserver.RequestHandler
input map[uint16]uint16
holding map[uint16]uint16
writes []hycWrite
}
func (h *hycHandler) HandleInputRegisters(req *mbserver.InputRegistersRequest) ([]uint16, error) {
res := make([]uint16, 0, req.Quantity)
for i := range req.Quantity {
v, ok := h.input[req.Addr+i]
if !ok {
return nil, mbserver.ErrIllegalDataAddress
}
res = append(res, v)
}
return res, nil
}
func (h *hycHandler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) ([]uint16, error) {
if req.IsWrite {
h.writes = append(h.writes, hycWrite{req.WriteFuncCode, req.Addr, req.Args})
for i, v := range req.Args {
h.holding[req.Addr+uint16(i)] = v
}
return req.Args, nil
}
res := make([]uint16, 0, req.Quantity)
for i := range req.Quantity {
res = append(res, h.holding[req.Addr+i])
}
return res, nil
}
// hycReg returns the absolute address of a connector register
func hycReg(connector, reg uint16) uint16 {
return connector*100 + reg
}
// hycRegs returns a fully populated connector input block with the given state
func hycRegs(connector, state uint16) map[uint16]uint16 {
regs := make(map[uint16]uint16)
for reg := range uint16(hycInputLength) {
regs[hycReg(connector, reg)] = 0
}
regs[hycReg(connector, hycRegState)] = state
return regs
}
// shared mock server: mbserver.Stop() races its accept goroutine, so the server
// is started once and never stopped; handler state is reset per test
var (
hycOnce sync.Once
hycURI string
hycSrvH = &hycHandler{RequestHandler: new(mbserver.DummyHandler)}
)
// hycTestCharger connects a charger to the shared mock Modbus server
func hycTestCharger(t *testing.T, connector uint16, regs map[uint16]uint16) (*AlpitronicHYC, *hycHandler) {
t.Helper()
return hycTestChargerWithLimit(t, connector, regs, nil)
}
// hycTestChargerWithLimit additionally seeds the connector's power limit
func hycTestChargerWithLimit(t *testing.T, connector uint16, regs, holding map[uint16]uint16) (*AlpitronicHYC, *hycHandler) {
t.Helper()
hycOnce.Do(func() {
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
srv, err := mbserver.New(hycSrvH)
require.NoError(t, err)
require.NoError(t, srv.Start(l))
hycURI = l.Addr().String()
})
if holding == nil {
holding = make(map[uint16]uint16)
}
hycSrvH.input = regs
hycSrvH.holding = holding
hycSrvH.writes = nil
conn, err := modbus.NewConnection(context.Background(), hycURI, "", "", 0, modbus.Tcp, 1)
require.NoError(t, err)
wb, err := newAlpitronicHYC(conn, connector)
require.NoError(t, err)
return wb, hycSrvH
}
func TestAlpitronicStatus(t *testing.T) {
tc := []struct {
state uint16
status api.ChargeStatus
err bool
}{
{hycStateAvailable, api.StatusA, false},
{hycStatePreparingTagIdReady, api.StatusA, false}, // authorized, nothing plugged in yet
{hycStatePreparingEVReady, api.StatusB, false},
{hycStateCharging, api.StatusC, false},
{hycStateSuspendedEV, api.StatusB, false},
{hycStateSuspendedEVSE, api.StatusB, false},
{hycStateFinishing, api.StatusB, false},
{hycStateReserved, api.StatusA, false},
{hycStateUnavailable, api.StatusA, false},
{hycStateUnavailableFwUpdate, api.StatusA, false},
{hycStateFaulted, api.StatusNone, true},
{hycStateUnavailableConnObj, api.StatusA, false},
{12, api.StatusNone, true},
}
for _, tc := range tc {
wb, _ := hycTestCharger(t, 1, hycRegs(1, 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 TestAlpitronicStatusReason(t *testing.T) {
tc := []struct {
state uint16
reason api.Reason
}{
{hycStateAvailable, api.ReasonUnknown},
{hycStatePreparingTagIdReady, api.ReasonUnknown},
{hycStatePreparingEVReady, api.ReasonWaitingForAuthorization},
{hycStateCharging, api.ReasonUnknown},
{hycStateFinishing, api.ReasonDisconnectRequired},
}
for _, tc := range tc {
wb, _ := hycTestCharger(t, 1, hycRegs(1, tc.state))
reason, err := wb.StatusReason()
require.NoError(t, err, "state %d", tc.state)
assert.Equal(t, tc.reason, reason, "state %d", tc.state)
}
}
func TestAlpitronicMeasurements(t *testing.T) {
regs := hycRegs(1, hycStateCharging)
regs[hycReg(1, hycRegChargingPower)+1] = 11500 // 11500 W
regs[hycReg(1, hycRegChargeTime)] = 3600 // 1h
regs[hycReg(1, hycRegChargedEnergy)] = 1234 // 12.34 kWh
regs[hycReg(1, hycRegSoC)] = 6550 // 65.5 %
regs[hycReg(1, hycRegTotalChargedEnergy)+3] = 18705 // 18.705 kWh
wb, _ := hycTestCharger(t, 1, regs)
power, err := wb.CurrentPower()
require.NoError(t, err)
assert.Equal(t, 11500.0, power)
dur, err := wb.ChargeDuration()
require.NoError(t, err)
assert.Equal(t, time.Hour, dur)
charged, err := wb.ChargedEnergy()
require.NoError(t, err)
assert.Equal(t, 12.34, charged)
soc, err := wb.Soc()
require.NoError(t, err)
assert.Equal(t, 65.5, soc)
total, err := wb.TotalEnergy()
require.NoError(t, err)
assert.Equal(t, 18.705, total)
}
func TestAlpitronicIdentify(t *testing.T) {
// no vehicle id, no tag
wb, _ := hycTestCharger(t, 1, hycRegs(1, hycStateAvailable))
id, err := wb.Identify()
require.NoError(t, err)
assert.Empty(t, id)
// vehicle id takes precedence over the tag
regs := hycRegs(1, hycStateCharging)
regs[hycReg(1, hycRegVID)+1] = 0xAABB
regs[hycReg(1, hycRegVID)+2] = 0xCCDD
regs[hycReg(1, hycRegVID)+3] = 0xEEFF
regs[hycReg(1, hycRegIdTag)] = 0x4142
wb, _ = hycTestCharger(t, 1, regs)
id, err = wb.Identify()
require.NoError(t, err)
assert.Equal(t, "0000aabbccddeeff", id)
// tag as fallback
regs = hycRegs(1, hycStateCharging)
regs[hycReg(1, hycRegIdTag)] = 0x4142
wb, _ = hycTestCharger(t, 1, regs)
id, err = wb.Identify()
require.NoError(t, err)
assert.Equal(t, "4142"+strings.Repeat("0", 36), id)
}
func TestAlpitronicEnable(t *testing.T) {
wb, h := hycTestCharger(t, 1, hycRegs(1, hycStatePreparingEVReady))
enabled, err := wb.Enabled()
require.NoError(t, err)
assert.False(t, enabled)
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)
// default current -> 1552W, disable -> the station must not see a zero limit
require.Len(t, h.writes, 2)
assert.Equal(t, hycWrite{16, hycReg(1, hycRegMaxPowerAC), []uint16{0, 1552}}, h.writes[0])
assert.Equal(t, hycWrite{16, hycReg(1, hycRegMaxPowerAC), []uint16{0, hycMinPowerAC}}, h.writes[1])
}
func TestAlpitronicMaxCurrent(t *testing.T) {
// current is written as power: A * 230V * 3p
tc := []struct {
current float64
args []uint16
}{
{hycMinCurrent, []uint16{0, 1552}}, // lowest accepted current
{6, []uint16{0, 4140}},
{16, []uint16{0, 11040}},
{200, []uint16{138000 >> 16, 138000 & 0xFFFF}},
}
for _, tc := range tc {
wb, h := hycTestCharger(t, 1, hycRegs(1, hycStateCharging))
require.NoError(t, wb.MaxCurrentMillis(tc.current))
require.Len(t, h.writes, 1, "current %v", tc.current)
assert.Equal(t, hycWrite{16, hycReg(1, hycRegMaxPowerAC), tc.args}, h.writes[0], "current %v", tc.current)
}
}
func TestAlpitronicSeedCurrent(t *testing.T) {
// the default must not read back as disabled
assert.Greater(t, hycPower(hycMinCurrent), uint32(hycMinPowerAC))
// no usable limit set -> default
wb, _ := hycTestCharger(t, 1, hycRegs(1, hycStateAvailable))
assert.Equal(t, hycMinCurrent, wb.curr)
// the disabled sentinel must not be taken over
holding := map[uint16]uint16{hycReg(1, hycRegMaxPowerAC) + 1: hycMinPowerAC}
wb, _ = hycTestChargerWithLimit(t, 1, hycRegs(1, hycStateAvailable), holding)
assert.Equal(t, hycMinCurrent, wb.curr)
// existing limit is adopted, so enabling does not fall back to the default
holding = map[uint16]uint16{hycReg(1, hycRegMaxPowerAC) + 1: 11040}
wb, h := hycTestChargerWithLimit(t, 1, hycRegs(1, hycStateCharging), holding)
assert.Equal(t, 16.0, wb.curr)
require.NoError(t, wb.Enable(true))
require.Len(t, h.writes, 1)
assert.Equal(t, hycWrite{16, hycReg(1, hycRegMaxPowerAC), []uint16{0, 11040}}, h.writes[0])
}
func TestAlpitronicMaxCurrentInvalid(t *testing.T) {
wb, h := hycTestCharger(t, 1, hycRegs(1, hycStateCharging))
// anything that does not exceed hycMinPowerAC would read back as disabled
assert.Error(t, wb.MaxCurrentMillis(2.24))
assert.Error(t, wb.MaxCurrentMillis(0))
assert.Error(t, wb.MaxCurrentMillis(-1))
assert.Empty(t, h.writes)
}
func TestAlpitronicConnector(t *testing.T) {
// second connector reads and writes the 2xx block
regs := hycRegs(2, hycStateCharging)
regs[hycReg(2, hycRegChargingPower)+1] = 50000
wb, h := hycTestCharger(t, 2, regs)
status, err := wb.Status()
require.NoError(t, err)
assert.Equal(t, api.StatusC, status)
power, err := wb.CurrentPower()
require.NoError(t, err)
assert.Equal(t, 50000.0, power)
require.NoError(t, wb.Enable(false))
require.Len(t, h.writes, 1)
assert.Equal(t, hycReg(2, hycRegMaxPowerAC), h.writes[0].addr)
}
func TestAlpitronicReadFailure(t *testing.T) {
// missing register in the bulk-read block -> IllegalDataAddress propagates
regs := hycRegs(1, hycStateCharging)
delete(regs, hycReg(1, hycRegTotalChargedEnergy)+3)
wb, _ := hycTestCharger(t, 1, regs)
_, err := wb.Status()
assert.Error(t, err)
_, err = wb.CurrentPower()
assert.Error(t, err)
}
func TestAlpitronicSponsorGate(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 := NewAlpitronicHYC(t.Context(), modbus.TcpSettings{URI: "localhost:0", ID: 1}, 1)
assert.ErrorIs(t, err, api.ErrSponsorRequired)
}