Files
energy-collector/alphaess.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()
}
}