Initial commit — energy collector (AlphaEss + SDM630 → TimescaleDB)
This commit is contained in:
137
alphaess.go
Normal file
137
alphaess.go
Normal file
@@ -0,0 +1,137 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user