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() } }