Voltie: rewrite Modbus driver for API v1.1 (#32671)
This commit is contained in:
parent
8e5322da22
commit
95fe545308
2 changed files with 354 additions and 82 deletions
|
|
@ -21,6 +21,7 @@ import (
|
|||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
|
|
@ -30,29 +31,191 @@ import (
|
|||
|
||||
// Voltie charger implementation
|
||||
// https://voltie.eu
|
||||
// Modbus API documentation v1.02
|
||||
// Modbus API documentation v1.1
|
||||
//
|
||||
// Modbus TCP is supported from EVSE firmware 350 and charger software 1.3.40.
|
||||
// Earlier firmware answers out-of-range reads with adjacent memory instead of
|
||||
// an exception and silently accepts ineffective FC16 writes. The driver checks
|
||||
// the firmware build number on startup; the charger software version is not
|
||||
// exposed over Modbus and has to be checked in the Voltie app.
|
||||
//
|
||||
// The same register map is served over RS-485 (Modbus RTU) and over the Modbus
|
||||
// TCP gateway. The gateway is a transparent bridge to the charger's MCU: it
|
||||
// forwards one request at a time, at most two transactions per second, and
|
||||
// abandons a request that is unanswered after 3s. The driver therefore fetches
|
||||
// the register blocks through the shared bulk read cache instead of issuing a
|
||||
// separate request per value, so an update cycle costs one request per block.
|
||||
//
|
||||
// Only function code 0x06 (write single register) is accepted for writes;
|
||||
// 0x10 (write multiple) is rejected with exception 0x01.
|
||||
|
||||
const (
|
||||
voltieRegChargerID = 0x0000 // R, INT16, Voltie Charger ID
|
||||
voltieRegFirmware = 0x0001 // R, INT16, FW version
|
||||
voltieRegStatus = 0x000A // R, INT16, EVSE_STATE
|
||||
voltieRegAutoStart = 0x000B // R/W, INT16, Auto Start enabled
|
||||
voltieRegChargingEnabled = 0x000C // R/W, INT16, Charging enabled
|
||||
voltieRegCharging = 0x000D // R, INT16, Charging (0=no charging, 1=charging)
|
||||
voltieRegPhases = 0x000E // R, INT16, Number of phases in use
|
||||
voltieRegStopReason = 0x0012 // R, INT16, Charge stop reason
|
||||
voltieRegCurrentLimit = 0x0014 // R/W, INT16, Software current limit [mA]
|
||||
// register blocks fetched in bulk
|
||||
voltieRegInfoBlock = 0x0000
|
||||
voltieLenInfoBlock = 10 // 0x0000..0x0009
|
||||
voltieRegStatusBlock = 0x000A
|
||||
voltieLenStatusBlock = 12 // 0x000A..0x0015
|
||||
voltieRegMeterBlock = 0x2000
|
||||
voltieLenMeterBlock = 22 // 0x2000..0x2015
|
||||
|
||||
voltieRegVoltages = 0x2000 // R, INT32, Phase L1 voltage [mV]
|
||||
voltieRegCurrents = 0x2006 // R, INT32, Phase L1 charging current [mA]
|
||||
voltieRegChargeDuration = 0x200C // R, INT32, Charge duration [s]
|
||||
voltieRegChargedEnergy = 0x200E // R, INT32, Charged energy in current session [Ws]
|
||||
voltieRegChargingPower = 0x2010 // R, INT32, Charging power [W]
|
||||
// identification block. The 64 bit serial numbers are sent
|
||||
// least-significant word first, unlike the metering values.
|
||||
voltieRegChargerID = 0x0000 // INT16 Voltie charger ID
|
||||
voltieRegFirmware = 0x0001 // INT16 EVSE firmware build number
|
||||
voltieRegMcuSerial = 0x0002 // INT64 MCU serial number
|
||||
voltieRegHpowSerial = 0x0006 // INT64 power board serial number
|
||||
voltieSerialRegCount = 4
|
||||
|
||||
// status block
|
||||
voltieRegStatus = 0x000A // INT16 EVSE_STATE
|
||||
voltieRegAutoStart = 0x000B // INT16 auto start enabled
|
||||
voltieRegChargeEnable = 0x000C // INT16 charging enabled
|
||||
voltieRegCharging = 0x000D // INT16 charging
|
||||
voltieRegPhases = 0x000E // INT16 number of phases in use
|
||||
voltieRegDlmSet = 0x000F // INT16 stored DLM mode
|
||||
voltieRegStopReason = 0x0012 // INT16 charge stop reason
|
||||
voltieRegCurrent = 0x0014 // INT16 software current limit [mA]
|
||||
voltieRegDlmEffective = 0x0015 // INT16 effective DLM mode
|
||||
|
||||
// meter block
|
||||
voltieRegVoltages = 0x2000 // 3x INT32 phase voltage [mV]
|
||||
voltieRegCurrents = 0x2006 // 3x INT32 phase charging current [mA]
|
||||
voltieRegDuration = 0x200C // INT32 charge duration [s]
|
||||
voltieRegEnergy = 0x200E // INT32 charged energy in session [Ws]
|
||||
voltieRegPower = 0x2010 // INT32 charging power [W]
|
||||
voltieRegCapacity = 0x2012 // INT32 instantaneous current capacity [mA]
|
||||
|
||||
// the charger's ampacity range [A]. The current limit register is typed
|
||||
// INT16, so the milliampere value must stay below the sign boundary.
|
||||
voltieMinCurrent = 6
|
||||
voltieMaxCurrent = 32
|
||||
|
||||
// firmware that fixes the Modbus slave address checks and rejects FC16
|
||||
voltieMinFirmware = 350
|
||||
|
||||
// the charger's documented default slave address
|
||||
voltieDefaultSlaveID = 11
|
||||
|
||||
// the gateway abandons a forwarded request after 3s, so the client must
|
||||
// wait longer than that to receive the resulting exception
|
||||
voltieTimeout = 5 * time.Second
|
||||
|
||||
// the gateway forwards at most two transactions per second
|
||||
voltieDelay = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
// EVSE states, see the "EVSE states" chapter of the Modbus API documentation
|
||||
const (
|
||||
voltieStateA = 0x01 // not connected
|
||||
voltieStateB = 0x02 // connected, ready
|
||||
voltieStateC = 0x03 // charging
|
||||
)
|
||||
|
||||
var voltieStates = map[uint16]string{
|
||||
0x00: "state not yet determined",
|
||||
0x01: "vehicle state A, not connected",
|
||||
0x02: "vehicle state B, connected",
|
||||
0x03: "vehicle state C, charging",
|
||||
0x04: "vehicle state D, charging with ventilation",
|
||||
0x05: "diode check failed",
|
||||
0x06: "GFCI fault",
|
||||
0x07: "bad ground",
|
||||
0x08: "relay stuck",
|
||||
0x09: "GFI self-test failure",
|
||||
0x0A: "over temperature",
|
||||
0x0B: "over current",
|
||||
0x0C: "hardware fault (voltage, current or temperature sensor)",
|
||||
0x0D: "vehicle state E, vehicle error",
|
||||
0x0E: "over humidity",
|
||||
0x0F: "input power phase misconnected",
|
||||
0x10: "overvoltage on the grid",
|
||||
0x11: "undervoltage on the grid",
|
||||
0x12: "charger disabled, not functioning",
|
||||
0x13: "booting",
|
||||
0x14: "no MID meter detected",
|
||||
0x15: "power board unidentified",
|
||||
0x18: "state undetermined",
|
||||
0x19: "uploading VoltieMeter firmware",
|
||||
}
|
||||
|
||||
// charge stop reasons reported by the MCU, see the "EVSE charge stop reasons"
|
||||
// chapter. Reasons 23..31 originate in the charger's control software and are
|
||||
// not reported through Modbus.
|
||||
var voltieStopReasons = map[uint16]string{
|
||||
1: "unspecified reason",
|
||||
2: "preset charge duration reached",
|
||||
3: "preset energy amount charged",
|
||||
4: "stopped by the user",
|
||||
5: "GFCI sensor tripped",
|
||||
6: "charger disabled, out of order",
|
||||
7: "firmware restart",
|
||||
8: "charger in sleep mode, out of order",
|
||||
9: "no voltage on the output (ground continuity or relay error)",
|
||||
10: "vehicle disconnected",
|
||||
11: "vehicle not accepting charge",
|
||||
12: "power board I2C bus fault",
|
||||
13: "GFCI self test failed",
|
||||
14: "over temperature",
|
||||
15: "diode error",
|
||||
16: "PE-N over-voltage",
|
||||
17: "relay stuck",
|
||||
18: "over current",
|
||||
21: "over humidity",
|
||||
22: "wrong phase order on the input",
|
||||
100: "not enough free building current available (dynamic load management)",
|
||||
101: "not enough solar current available (eco/green mode)",
|
||||
102: "grid voltage is not high enough (grid-controlled mode)",
|
||||
103: "charge current limit set to zero",
|
||||
104: "no MID meter available",
|
||||
105: "overvoltage",
|
||||
106: "vehicle error",
|
||||
107: "undervoltage",
|
||||
108: "vehicle in state D while state D is disabled",
|
||||
109: "power board unidentified",
|
||||
}
|
||||
|
||||
// Voltie is an api.Charger implementation for Voltie wallboxes
|
||||
type Voltie struct {
|
||||
conn *modbus.Connection
|
||||
conn *modbus.Connection
|
||||
log *util.Logger
|
||||
cache *modbus.Cache
|
||||
status modbus.Block
|
||||
meter modbus.Block
|
||||
info modbus.Block
|
||||
}
|
||||
|
||||
// read fetches a register block through the shared bulk read cache, so all
|
||||
// values taken from the same block within a poll cycle cost one request
|
||||
func (wb *Voltie) read(block modbus.Block) ([]byte, error) {
|
||||
key := fmt.Sprintf("%s/holding/%d/%d", wb.conn.Addr(), block.Register, block.Count)
|
||||
|
||||
payload, _, err := wb.cache.Fetch(key, func() ([]byte, error) {
|
||||
return wb.conn.ReadHoldingRegisters(block.Register, block.Count)
|
||||
})
|
||||
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// voltieSerial decodes a 64 bit serial number, which is sent least-significant
|
||||
// word first unlike the 32 bit metering values
|
||||
func voltieSerial(b []byte, off int) uint64 {
|
||||
var res uint64
|
||||
for i := range voltieSerialRegCount {
|
||||
res |= uint64(binary.BigEndian.Uint16(b[off+2*i:])) << (16 * i)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// voltieU16 returns the register at addr within a cached block payload
|
||||
func voltieU16(block modbus.Block, b []byte, addr uint16) uint16 {
|
||||
return binary.BigEndian.Uint16(b[block.ByteOffset(addr):])
|
||||
}
|
||||
|
||||
// voltieU32 returns the 32 bit value at addr within a cached block payload,
|
||||
// most-significant word first
|
||||
func voltieU32(block modbus.Block, b []byte, addr uint16) uint32 {
|
||||
return binary.BigEndian.Uint32(b[block.ByteOffset(addr):])
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -61,19 +224,27 @@ func init() {
|
|||
|
||||
// NewVoltieFromConfig creates a Voltie charger from generic config
|
||||
func NewVoltieFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
|
||||
cc := modbus.TcpSettings{
|
||||
ID: 1,
|
||||
cc := struct {
|
||||
modbus.TcpSettings `mapstructure:",squash"`
|
||||
Cache time.Duration
|
||||
}{
|
||||
TcpSettings: modbus.TcpSettings{
|
||||
ID: voltieDefaultSlaveID,
|
||||
Timeout: voltieTimeout,
|
||||
Delay: voltieDelay,
|
||||
},
|
||||
Cache: time.Second,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewVoltie(ctx, cc)
|
||||
return NewVoltie(ctx, cc.TcpSettings, cc.Cache)
|
||||
}
|
||||
|
||||
// NewVoltie creates a Voltie charger
|
||||
func NewVoltie(ctx context.Context, settings modbus.TcpSettings) (*Voltie, error) {
|
||||
func NewVoltie(ctx context.Context, settings modbus.TcpSettings, cache time.Duration) (*Voltie, error) {
|
||||
conn, err := settings.Connection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -87,55 +258,97 @@ func NewVoltie(ctx context.Context, settings modbus.TcpSettings) (*Voltie, error
|
|||
conn.Logger(log.TRACE)
|
||||
|
||||
wb := &Voltie{
|
||||
conn: conn,
|
||||
conn: conn,
|
||||
log: log,
|
||||
cache: modbus.NewCache(cache),
|
||||
info: modbus.Block{Register: voltieRegInfoBlock, Count: voltieLenInfoBlock},
|
||||
status: modbus.Block{Register: voltieRegStatusBlock, Count: voltieLenStatusBlock},
|
||||
meter: modbus.Block{Register: voltieRegMeterBlock, Count: voltieLenMeterBlock},
|
||||
}
|
||||
|
||||
// Disable auto start
|
||||
if _, err := wb.conn.WriteSingleRegister(voltieRegAutoStart, 0); err != nil {
|
||||
if b, err := wb.read(wb.info); err == nil {
|
||||
if fw := voltieU16(wb.info, b, voltieRegFirmware); fw < voltieMinFirmware {
|
||||
log.WARN.Printf("firmware %d is outdated, Modbus TCP requires %d or later", fw, voltieMinFirmware)
|
||||
}
|
||||
}
|
||||
|
||||
if err := wb.checkSettings(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wb, nil
|
||||
}
|
||||
|
||||
// checkSettings inspects the charger's settings once on startup. The charger
|
||||
// must not start a session on its own while evcc is in control, and its own
|
||||
// load management would silently cap the current requested by evcc.
|
||||
func (wb *Voltie) checkSettings() error {
|
||||
b, err := wb.read(wb.status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dlm := voltieU16(wb.status, b, voltieRegDlmEffective); dlm != 0 {
|
||||
wb.log.WARN.Printf("charger-side load management is active (mode %d) and will cap the requested current", dlm)
|
||||
}
|
||||
|
||||
// the auto start setting is persisted in the charger's EEPROM and stays off
|
||||
// after evcc is removed, so it is only written when actually enabled
|
||||
if voltieU16(wb.status, b, voltieRegAutoStart) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := wb.conn.WriteSingleRegister(voltieRegAutoStart, 0); err != nil {
|
||||
return fmt.Errorf("disable auto start: %w (is Modbus control enabled on the charger?)", err)
|
||||
}
|
||||
|
||||
wb.cache.Clear()
|
||||
wb.log.WARN.Println("auto start disabled, the setting is persistent and must be restored in the Voltie app when evcc is removed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status implements the api.Charger interface
|
||||
func (wb *Voltie) Status() (api.ChargeStatus, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(voltieRegStatus, 1)
|
||||
b, err := wb.read(wb.status)
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
status := binary.BigEndian.Uint16(b)
|
||||
|
||||
// EVSE states:
|
||||
// 0x01: vehicle in state A – not connected
|
||||
// 0x02: vehicle in state B – connected, ready
|
||||
// 0x03: vehicle in state C – charging
|
||||
// 0x04: vehicle in state D – charging, ventilation required
|
||||
// 0x0D: vehicle in state E – vehicle error
|
||||
// 0x05-0x0C, 0x0E-0x11: internal error states
|
||||
// 0xFF: charger disabled, not functioning
|
||||
|
||||
switch status {
|
||||
case 0x01:
|
||||
switch state := voltieU16(wb.status, b, voltieRegStatus); state {
|
||||
case voltieStateA:
|
||||
return api.StatusA, nil
|
||||
case 0x02:
|
||||
case voltieStateB:
|
||||
return api.StatusB, nil
|
||||
case 0x03, 0x04:
|
||||
case voltieStateC:
|
||||
return api.StatusC, nil
|
||||
default:
|
||||
return api.StatusNone, fmt.Errorf("invalid status: %0x", status)
|
||||
// any other state, including D where the vehicle requires ventilation,
|
||||
// is reported as an error together with the MCU's stop reason
|
||||
desc, ok := voltieStates[state]
|
||||
if !ok {
|
||||
desc = "unknown state"
|
||||
}
|
||||
|
||||
if reason := voltieU16(wb.status, b, voltieRegStopReason); reason != 0 {
|
||||
if txt, ok := voltieStopReasons[reason]; ok {
|
||||
return api.StatusNone, fmt.Errorf("%s (0x%02X): %s", desc, state, txt)
|
||||
}
|
||||
return api.StatusNone, fmt.Errorf("%s (0x%02X): stop reason %d", desc, state, reason)
|
||||
}
|
||||
|
||||
return api.StatusNone, fmt.Errorf("%s (0x%02X)", desc, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (wb *Voltie) Enabled() (bool, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(voltieRegChargingEnabled, 1)
|
||||
b, err := wb.read(wb.status)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return binary.BigEndian.Uint16(b) != 0, nil
|
||||
return voltieU16(wb.status, b, voltieRegChargeEnable) != 0, nil
|
||||
}
|
||||
|
||||
// Enable implements the api.Charger interface
|
||||
|
|
@ -145,7 +358,11 @@ func (wb *Voltie) Enable(enable bool) error {
|
|||
u = 1
|
||||
}
|
||||
|
||||
_, err := wb.conn.WriteSingleRegister(voltieRegChargingEnabled, u)
|
||||
_, err := wb.conn.WriteSingleRegister(voltieRegChargeEnable, u)
|
||||
if err == nil {
|
||||
wb.cache.Clear()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -158,91 +375,132 @@ var _ api.ChargerEx = (*Voltie)(nil)
|
|||
|
||||
// MaxCurrentMillis implements the api.ChargerEx interface
|
||||
func (wb *Voltie) MaxCurrentMillis(current float64) error {
|
||||
if current < 6 {
|
||||
if current < voltieMinCurrent || current > voltieMaxCurrent {
|
||||
return fmt.Errorf("invalid current %.1f", current)
|
||||
}
|
||||
|
||||
_, err := wb.conn.WriteSingleRegister(voltieRegCurrentLimit, uint16(current*1000))
|
||||
_, err := wb.conn.WriteSingleRegister(voltieRegCurrent, uint16(current*1e3))
|
||||
if err == nil {
|
||||
wb.cache.Clear()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ api.CurrentGetter = (*Voltie)(nil)
|
||||
|
||||
// GetMaxCurrent implements the api.CurrentGetter interface
|
||||
func (wb *Voltie) GetMaxCurrent() (float64, error) {
|
||||
b, err := wb.read(wb.status)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return float64(voltieU16(wb.status, b, voltieRegCurrent)) / 1e3, nil
|
||||
}
|
||||
|
||||
var _ api.Meter = (*Voltie)(nil)
|
||||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
func (wb *Voltie) CurrentPower() (float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(voltieRegChargingPower, 2)
|
||||
b, err := wb.read(wb.meter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return float64(binary.BigEndian.Uint32(b)), nil
|
||||
return float64(int32(voltieU32(wb.meter, b, voltieRegPower))), nil
|
||||
}
|
||||
|
||||
var _ api.ChargeRater = (*Voltie)(nil)
|
||||
|
||||
// ChargedEnergy implements the api.ChargeRater interface
|
||||
func (wb *Voltie) ChargedEnergy() (float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(voltieRegChargedEnergy, 2)
|
||||
b, err := wb.read(wb.meter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return float64(binary.BigEndian.Uint32(b)) / 3.6e6, nil // Ws to kWh
|
||||
return float64(voltieU32(wb.meter, b, voltieRegEnergy)) / 3.6e6, nil // Ws to kWh
|
||||
}
|
||||
|
||||
var _ api.ChargeTimer = (*Voltie)(nil)
|
||||
|
||||
// ChargeDuration implements the api.ChargeTimer interface
|
||||
func (wb *Voltie) ChargeDuration() (time.Duration, error) {
|
||||
b, err := wb.read(wb.meter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return time.Duration(voltieU32(wb.meter, b, voltieRegDuration)) * time.Second, nil
|
||||
}
|
||||
|
||||
// getPhaseValues returns 3 sequential 32 bit values from the meter block, scaled from milli units
|
||||
func (wb *Voltie) getPhaseValues(reg uint16) (float64, float64, float64, error) {
|
||||
b, err := wb.read(wb.meter)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
var res [3]float64
|
||||
for i := range res {
|
||||
res[i] = float64(voltieU32(wb.meter, b, reg+uint16(2*i))) / 1e3
|
||||
}
|
||||
|
||||
return res[0], res[1], res[2], nil
|
||||
}
|
||||
|
||||
var _ api.PhaseCurrents = (*Voltie)(nil)
|
||||
|
||||
// Currents implements the api.PhaseCurrents interface
|
||||
func (wb *Voltie) Currents() (float64, float64, float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(voltieRegCurrents, 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:])) / 1e3 // mA to A
|
||||
}
|
||||
|
||||
return res[0], res[1], res[2], nil
|
||||
return wb.getPhaseValues(voltieRegCurrents)
|
||||
}
|
||||
|
||||
var _ api.PhaseVoltages = (*Voltie)(nil)
|
||||
|
||||
// Voltages implements the api.PhaseVoltages interface
|
||||
func (wb *Voltie) Voltages() (float64, float64, float64, error) {
|
||||
b, err := wb.conn.ReadHoldingRegisters(voltieRegVoltages, 6)
|
||||
return wb.getPhaseValues(voltieRegVoltages)
|
||||
}
|
||||
|
||||
var _ api.PhaseGetter = (*Voltie)(nil)
|
||||
|
||||
// GetPhases implements the api.PhaseGetter interface
|
||||
func (wb *Voltie) GetPhases() (int, error) {
|
||||
b, err := wb.read(wb.status)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var res [3]float64
|
||||
for i := range res {
|
||||
res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / 1e3 // mV to V
|
||||
}
|
||||
|
||||
return res[0], res[1], res[2], nil
|
||||
return int(voltieU16(wb.status, b, voltieRegPhases)), nil
|
||||
}
|
||||
|
||||
var _ api.Diagnosis = (*Voltie)(nil)
|
||||
|
||||
// Diagnose implements the api.Diagnosis interface
|
||||
func (wb *Voltie) Diagnose() {
|
||||
if b, err := wb.conn.ReadHoldingRegisters(voltieRegChargerID, 1); err == nil {
|
||||
fmt.Printf("\tCharger ID:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
if b, err := wb.read(wb.info); err == nil {
|
||||
fmt.Printf("\tCharger ID:\t%d\n", voltieU16(wb.info, b, voltieRegChargerID))
|
||||
fmt.Printf("\tFirmware:\t%d\n", voltieU16(wb.info, b, voltieRegFirmware))
|
||||
fmt.Printf("\tMCU serial:\t%d\n", voltieSerial(b, wb.info.ByteOffset(voltieRegMcuSerial)))
|
||||
fmt.Printf("\tPower serial:\t%d\n", voltieSerial(b, wb.info.ByteOffset(voltieRegHpowSerial)))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(voltieRegFirmware, 1); err == nil {
|
||||
fmt.Printf("\tFirmware:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
|
||||
if b, err := wb.read(wb.status); err == nil {
|
||||
state := voltieU16(wb.status, b, voltieRegStatus)
|
||||
fmt.Printf("\tStatus:\t\t0x%02X (%s)\n", state, voltieStates[state])
|
||||
fmt.Printf("\tAuto start:\t%d\n", voltieU16(wb.status, b, voltieRegAutoStart))
|
||||
fmt.Printf("\tCharging:\t%d\n", voltieU16(wb.status, b, voltieRegCharging))
|
||||
fmt.Printf("\tPhases:\t\t%d\n", voltieU16(wb.status, b, voltieRegPhases))
|
||||
fmt.Printf("\tDLM mode:\t%d set, %d effective\n", voltieU16(wb.status, b, voltieRegDlmSet), voltieU16(wb.status, b, voltieRegDlmEffective))
|
||||
fmt.Printf("\tCurrent limit:\t%d mA\n", voltieU16(wb.status, b, voltieRegCurrent))
|
||||
|
||||
reason := voltieU16(wb.status, b, voltieRegStopReason)
|
||||
fmt.Printf("\tStop reason:\t%d (%s)\n", reason, voltieStopReasons[reason])
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(voltieRegStatus, 1); err == nil {
|
||||
fmt.Printf("\tStatus:\t\t0x%04X\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(voltieRegPhases, 1); err == nil {
|
||||
fmt.Printf("\tPhases:\t\t%d\n", binary.BigEndian.Uint16(b))
|
||||
}
|
||||
if b, err := wb.conn.ReadHoldingRegisters(voltieRegStopReason, 1); err == nil {
|
||||
fmt.Printf("\tStop reason:\t%d\n", binary.BigEndian.Uint16(b))
|
||||
|
||||
if b, err := wb.read(wb.meter); err == nil {
|
||||
fmt.Printf("\tCapacity:\t%d mA\n", voltieU32(wb.meter, b, voltieRegCapacity))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,25 @@ products:
|
|||
generic: Charger
|
||||
capabilities: ["mA", "meter", "dim"]
|
||||
requirements:
|
||||
description:
|
||||
de: Modbus muss in der Voltie App im Modbus-Konfigurationsbildschirm auf "TCP" gestellt werden; die dort eingestellte Slave-Adresse (Standard 11) muss mit der ID übereinstimmen. Der Kommunikations-Timeout regelt die Ladung nach Verbindungsverlust auf 0 A herunter, er muss daher auf 0 gesetzt (deaktiviert) oder größer als das evcc-Aktualisierungsintervall gewählt werden. Benötigt EVSE-Firmware 350 oder neuer und Ladegerät-Software 1.3.40 oder neuer.
|
||||
en: Modbus must be set to "TCP" on the Modbus config screen of the Voltie app, and the slave address configured there (default 11) must match the ID. The communication timeout reduces the charging current to 0 A after a loss of communication, so it has to be set to 0 (disabled) or to a value larger than the evcc update interval. Requires EVSE firmware 350 or later and charger software 1.3.40 or later.
|
||||
evcc: ["sponsorship"]
|
||||
caveats:
|
||||
- description:
|
||||
de: Der Auto-Start des Ladegeräts wird beim Start von evcc dauerhaft deaktiviert und muss nach dem Entfernen von evcc in der Voltie App wieder eingeschaltet werden.
|
||||
en: The charger's auto start mode is disabled persistently when evcc starts and has to be switched back on in the Voltie app after evcc is removed.
|
||||
- description:
|
||||
de: Das Modbus-TCP-Gateway leitet höchstens zwei Transaktionen pro Sekunde weiter. Kürzere Abfrageintervalle führen zu Zeitüberschreitungen, nicht zu aktuelleren Daten.
|
||||
en: The Modbus TCP gateway forwards at most two transactions per second. Shorter polling intervals produce timeouts rather than fresher data.
|
||||
params:
|
||||
- name: modbus
|
||||
choice: ["tcpip"]
|
||||
id: 1
|
||||
id: 11
|
||||
- name: cache
|
||||
advanced: true
|
||||
default: 1s
|
||||
render: |
|
||||
type: voltie
|
||||
{{- include "modbus" . }}
|
||||
cache: {{ .cache }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue