138 lines
3.7 KiB
Go
138 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/simonvetter/modbus"
|
|
)
|
|
|
|
// InverterData holds one poll's worth of AlphaEss readings.
|
|
type InverterData struct {
|
|
Pv1Power float32 // W
|
|
Pv2Power float32 // W
|
|
PvL1Power float32 // W
|
|
PvL2Power float32 // W
|
|
PvL3Power float32 // W
|
|
BatterySoC float32 // %
|
|
GridImportKwh float32 // kWh cumulative
|
|
GridExportKwh float32 // kWh cumulative
|
|
PvEnergyKwh float32 // kWh cumulative
|
|
}
|
|
|
|
// alphaReg describes one AlphaEss Modbus holding register.
|
|
// All AlphaEss values are signed int32 (qty=2 → two 16-bit words, big-endian)
|
|
// or unsigned int16 (qty=1), then multiplied by Factor.
|
|
type alphaReg struct {
|
|
addr uint16
|
|
qty uint16
|
|
factor float32
|
|
}
|
|
|
|
// Register addresses taken from the AlphaEss Modbus spec (and validated by pvcollect).
|
|
var alphaRegs = struct {
|
|
gridExport, gridImport alphaReg
|
|
pv1, pv2 alphaReg
|
|
pvL1, pvL2, pvL3 alphaReg
|
|
pvEnergy alphaReg
|
|
batterySoC alphaReg
|
|
}{
|
|
gridExport: alphaReg{16, 2, 0.01},
|
|
gridImport: alphaReg{18, 2, 0.01},
|
|
pv1: alphaReg{1055, 2, 1},
|
|
pv2: alphaReg{1059, 2, 1},
|
|
pvL1: alphaReg{1030, 2, 1},
|
|
pvL2: alphaReg{1032, 2, 1},
|
|
pvL3: alphaReg{1034, 2, 1},
|
|
pvEnergy: alphaReg{1086, 2, 0.1},
|
|
batterySoC: alphaReg{258, 1, 0.1},
|
|
}
|
|
|
|
// AlphaEss polls the AlphaEss inverter via Modbus TCP.
|
|
type AlphaEss struct {
|
|
cfg AlphaConf
|
|
client *modbus.ModbusClient
|
|
}
|
|
|
|
func NewAlphaEss(cfg AlphaConf) (*AlphaEss, error) {
|
|
a := &AlphaEss{cfg: cfg}
|
|
if err := a.connect(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (a *AlphaEss) connect() error {
|
|
url := fmt.Sprintf("tcp://%s:%d", a.cfg.Host, a.cfg.Port)
|
|
c, err := modbus.NewClient(&modbus.ClientConfiguration{URL: url})
|
|
if err != nil {
|
|
return fmt.Errorf("modbus client: %w", err)
|
|
}
|
|
c.SetUnitId(a.cfg.SlaveID)
|
|
if err := c.Open(); err != nil {
|
|
return fmt.Errorf("modbus open: %w", err)
|
|
}
|
|
a.client = c
|
|
return nil
|
|
}
|
|
|
|
func (a *AlphaEss) reconnect() {
|
|
if a.client != nil {
|
|
a.client.Close()
|
|
}
|
|
if err := a.connect(); err != nil {
|
|
log.Printf("alphaess reconnect: %v", err)
|
|
}
|
|
}
|
|
|
|
func (a *AlphaEss) readReg(r alphaReg) (float32, error) {
|
|
regs, err := a.client.ReadRegisters(r.addr, r.qty, modbus.HOLDING_REGISTER)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var raw int32
|
|
switch r.qty {
|
|
case 1:
|
|
raw = int32(regs[0])
|
|
case 2:
|
|
raw = int32(regs[0])<<16 | int32(regs[1])
|
|
}
|
|
return float32(raw) * r.factor, nil
|
|
}
|
|
|
|
// Poll reads all AlphaEss registers. On connection error it attempts one
|
|
// reconnect before returning the error.
|
|
func (a *AlphaEss) Poll() (*InverterData, error) {
|
|
data, err := a.poll()
|
|
if err != nil {
|
|
log.Printf("alphaess poll error, reconnecting: %v", err)
|
|
a.reconnect()
|
|
data, err = a.poll()
|
|
}
|
|
return data, err
|
|
}
|
|
|
|
func (a *AlphaEss) poll() (*InverterData, error) {
|
|
r := alphaRegs
|
|
read := func(reg alphaReg) (float32, error) { return a.readReg(reg) }
|
|
|
|
d := &InverterData{}
|
|
var err error
|
|
if d.GridExportKwh, err = read(r.gridExport); err != nil { return nil, err }
|
|
if d.GridImportKwh, err = read(r.gridImport); err != nil { return nil, err }
|
|
if d.Pv1Power, err = read(r.pv1); err != nil { return nil, err }
|
|
if d.Pv2Power, err = read(r.pv2); err != nil { return nil, err }
|
|
if d.PvL1Power, err = read(r.pvL1); err != nil { return nil, err }
|
|
if d.PvL2Power, err = read(r.pvL2); err != nil { return nil, err }
|
|
if d.PvL3Power, err = read(r.pvL3); err != nil { return nil, err }
|
|
if d.PvEnergyKwh, err = read(r.pvEnergy); err != nil { return nil, err }
|
|
if d.BatterySoC, err = read(r.batterySoC); err != nil { return nil, err }
|
|
return d, nil
|
|
}
|
|
|
|
func (a *AlphaEss) Close() {
|
|
if a.client != nil {
|
|
a.client.Close()
|
|
}
|
|
}
|