diff --git a/charger/alpitronic.go b/charger/alpitronic.go index 5e7169aa4..e55a64a86 100644 --- a/charger/alpitronic.go +++ b/charger/alpitronic.go @@ -36,6 +36,7 @@ type AlpitronicHYC struct { conn *modbus.Connection inputG func() ([]byte, error) curr float64 + enabled bool connector uint16 } @@ -61,22 +62,20 @@ const ( const ( hycRegState = 0 // UINT16 hycRegChargingVoltage = 1 // UINT32, cV - hycRegChargingCurrent = 3 // UINT16, cA - hycRegChargingPower = 4 // UINT32, W + hycRegChargingCurrent = 3 // UINT16, cA up to 3.0, A from 3.1.0 on + hycRegPowerAbsorptionAC = 4 // UINT32, W- DC charging power up to 3.0, AC absorption from 3.1.0 on hycRegChargeTime = 6 // UINT16, s - hycRegChargedEnergy = 7 // UINT16, kWh/100 + hycRegChargedEnergy = 7 // UINT16, kWh/100 up to 3.0, kWh/10 from 3.1.0 on 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 + hycRegMaxPowerAbsorption = 10 // UINT32, W- includes the limit written by us + hycRegMinPowerAbsorption = 12 // UINT32, W 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) + // read up to the last register in use (x32..x35). x14/x16 (VAR) are covered but + // no longer documented since 3.1.0- to be verified against the device hycInputLength = 36 ) @@ -104,7 +103,7 @@ const ( // 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 +// for any connector to actually charge- see hycRegMinPowerAbsorption const ( hycPowerPerAmp = 230 * 3 // W/A on the AC side hycMinPowerAC = 1547 // W @@ -174,6 +173,7 @@ func newAlpitronicHYC(conn *modbus.Connection, connector uint16) (*AlpitronicHYC if power := encoding.Uint32(b); power > hycMinPowerAC { wb.curr = float64(power) / hycPowerPerAmp + wb.enabled = true } return wb, nil @@ -200,6 +200,12 @@ func (wb *AlpitronicHYC) setPower(power uint32) error { encoding.PutUint32(b, power) _, err := wb.conn.WriteMultipleRegisters(wb.reg(hycRegMaxPowerAC), 2, b) + if err == nil { + // Status is polled before Enabled, so track our own writes to keep + // the charging state from lagging a cycle behind + wb.enabled = power > hycMinPowerAC + } + return err } @@ -241,6 +247,9 @@ func (wb *AlpitronicHYC) Status() (api.ChargeStatus, error) { hycStateFinishing: return api.StatusB, nil case hycStateCharging: + if !wb.enabled { + return api.StatusB, nil + } return api.StatusC, nil case hycStateFaulted: return api.StatusNone, errors.New("connector state: faulted") @@ -249,6 +258,25 @@ func (wb *AlpitronicHYC) Status() (api.ChargeStatus, error) { } } +var _ api.StatusReasoner = (*AlpitronicHYC)(nil) + +// StatusReason implements the api.StatusReasoner interface +func (wb *AlpitronicHYC) StatusReason() (api.Reason, error) { + s, err := wb.state() + if err != nil { + return api.ReasonUnknown, err + } + + switch s { + case hycStatePreparingEVReady: + return api.ReasonWaitingForAuthorization, nil + case hycStateFinishing: + return api.ReasonDisconnectRequired, nil + default: + return api.ReasonUnknown, nil + } +} + // Enabled implements the api.Charger interface func (wb *AlpitronicHYC) Enabled() (bool, error) { b, err := wb.conn.ReadHoldingRegisters(wb.reg(hycRegMaxPowerAC), 2) @@ -256,7 +284,9 @@ func (wb *AlpitronicHYC) Enabled() (bool, error) { return false, err } - return encoding.Uint32(b) > hycMinPowerAC, nil + wb.enabled = encoding.Uint32(b) > hycMinPowerAC + + return wb.enabled, nil } // Enable implements the api.Charger interface @@ -303,7 +333,7 @@ func (wb *AlpitronicHYC) CurrentPower() (float64, error) { return 0, err } - return float64(encoding.Uint32(hycInput(b, hycRegChargingPower, 4))), nil + return float64(encoding.Uint32(hycInput(b, hycRegPowerAbsorptionAC, 4))), nil } var _ api.ChargeTimer = (*AlpitronicHYC)(nil) @@ -318,17 +348,9 @@ func (wb *AlpitronicHYC) ChargeDuration() (time.Duration, error) { 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.inputG() - if err != nil { - return 0, err - } - - return float64(encoding.Uint16(hycInput(b, hycRegChargedEnergy, 2))) / 100, nil -} +// api.ChargeRater is not implemented: hycRegChargedEnergy is scaled kWh/100 up to +// firmware 3.0 and kWh/10 from 3.1.0 on, so the session energy is derived from +// TotalEnergy instead var _ api.MeterEnergy = (*AlpitronicHYC)(nil) @@ -342,25 +364,6 @@ func (wb *AlpitronicHYC) TotalEnergy() (float64, error) { 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) { - s, err := wb.state() - if err != nil { - return api.ReasonUnknown, err - } - - switch s { - case hycStatePreparingEVReady: - return api.ReasonWaitingForAuthorization, nil - case hycStateFinishing: - return api.ReasonDisconnectRequired, nil - default: - return api.ReasonUnknown, nil - } -} - var _ api.Identifier = (*AlpitronicHYC)(nil) // Identify implements the api.Identifier interface diff --git a/charger/alpitronic_test.go b/charger/alpitronic_test.go index 5d16b226c..3f1ce0574 100644 --- a/charger/alpitronic_test.go +++ b/charger/alpitronic_test.go @@ -130,7 +130,7 @@ func TestAlpitronicStatus(t *testing.T) { {hycStateAvailable, api.StatusA, false}, {hycStatePreparingTagIdReady, api.StatusA, false}, // authorized, nothing plugged in yet {hycStatePreparingEVReady, api.StatusB, false}, - {hycStateCharging, api.StatusC, false}, + {hycStateCharging, api.StatusB, false}, // charging while disabled, see below {hycStateSuspendedEV, api.StatusB, false}, {hycStateSuspendedEVSE, api.StatusB, false}, {hycStateFinishing, api.StatusB, false}, @@ -153,6 +153,14 @@ func TestAlpitronicStatus(t *testing.T) { } assert.Equal(t, tc.status, status, "state %d", tc.state) } + + // charging while enabled + holding := map[uint16]uint16{hycReg(1, hycRegMaxPowerAC) + 1: 11040} + wb, _ := hycTestChargerWithLimit(t, 1, hycRegs(1, hycStateCharging), holding) + + status, err := wb.Status() + require.NoError(t, err) + assert.Equal(t, api.StatusC, status) } func TestAlpitronicStatusReason(t *testing.T) { @@ -178,9 +186,8 @@ func TestAlpitronicStatusReason(t *testing.T) { func TestAlpitronicMeasurements(t *testing.T) { regs := hycRegs(1, hycStateCharging) - regs[hycReg(1, hycRegChargingPower)+1] = 11500 // 11500 W + regs[hycReg(1, hycRegPowerAbsorptionAC)+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 @@ -194,10 +201,6 @@ func TestAlpitronicMeasurements(t *testing.T) { 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) @@ -319,9 +322,10 @@ func TestAlpitronicMaxCurrentInvalid(t *testing.T) { func TestAlpitronicConnector(t *testing.T) { // second connector reads and writes the 2xx block regs := hycRegs(2, hycStateCharging) - regs[hycReg(2, hycRegChargingPower)+1] = 50000 + regs[hycReg(2, hycRegPowerAbsorptionAC)+1] = 50000 - wb, h := hycTestCharger(t, 2, regs) + holding := map[uint16]uint16{hycReg(2, hycRegMaxPowerAC) + 1: 50000} + wb, h := hycTestChargerWithLimit(t, 2, regs, holding) status, err := wb.Status() require.NoError(t, err)