Initial implementation: MQTT-based energy meter subscriber
Subscribes to Eastron SDM630 power meter data via MQTT broker, decodes Modbus RTU frames, and writes readings to InfluxDB. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
93
poller.go
Normal file
93
poller.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||
)
|
||||
|
||||
const responseTimeout = 2 * time.Second
|
||||
|
||||
// Poller periodically publishes FC4 read requests for every configured register
|
||||
// on every device, waiting for each response before sending the next request.
|
||||
// After all registers for a device are collected, one InfluxDB point is written.
|
||||
type Poller struct {
|
||||
requester *Requester
|
||||
handler *handler
|
||||
writeAPI api.WriteAPIBlocking
|
||||
org string
|
||||
bucket string
|
||||
devices []DeviceConfig
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewPoller(r *Requester, h *handler, writeAPI api.WriteAPIBlocking, cfg Config, devices []DeviceConfig, interval time.Duration) *Poller {
|
||||
return &Poller{
|
||||
requester: r,
|
||||
handler: h,
|
||||
writeAPI: writeAPI,
|
||||
org: cfg.InfluxOrg,
|
||||
bucket: cfg.InfluxBucket,
|
||||
devices: devices,
|
||||
interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
// Run blocks until ctx is cancelled, polling immediately on start.
|
||||
func (p *Poller) Run(ctx context.Context) {
|
||||
p.poll(ctx)
|
||||
ticker := time.NewTicker(p.interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
p.poll(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller) poll(ctx context.Context) {
|
||||
now := time.Now()
|
||||
for _, dev := range p.devices {
|
||||
point := influxdb2.NewPointWithMeasurement(dev.Name).SetTime(now)
|
||||
hasFields := false
|
||||
|
||||
for _, reg := range dev.Registers {
|
||||
if reg.Type != "input" {
|
||||
logger.Printf("skipping holding register %s/%s (FC3 not implemented)", dev.Name, reg.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Register the waiter before publishing to avoid a race with a fast response.
|
||||
respCh := p.handler.waitForResponse(dev.SlaveAddress)
|
||||
|
||||
if err := p.requester.ReadInputRegisters(dev.Segment, byte(dev.SlaveAddress), reg.Address, reg.Count); err != nil {
|
||||
logger.Printf("poll %s/%s: %v", dev.Name, reg.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case readings := <-respCh:
|
||||
for _, r := range readings {
|
||||
point.AddField(r.Name, r.Value)
|
||||
hasFields = true
|
||||
}
|
||||
case <-time.After(responseTimeout):
|
||||
logger.Printf("timeout waiting for response: %s/%s", dev.Name, reg.Name)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFields {
|
||||
continue
|
||||
}
|
||||
if err := p.writeAPI.WritePoint(ctx, point); err != nil {
|
||||
logger.Printf("influx write %s: %v", dev.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user