142 lines
3.4 KiB
Go
142 lines
3.4 KiB
Go
package meter
|
|
|
|
import (
|
|
"github.com/andig/evcc/api"
|
|
"github.com/andig/evcc/provider"
|
|
"github.com/andig/evcc/provider/modbus"
|
|
"github.com/andig/evcc/util"
|
|
"github.com/volkszaehler/mbmd/meters"
|
|
"github.com/volkszaehler/mbmd/meters/rs485"
|
|
"github.com/volkszaehler/mbmd/meters/sunspec"
|
|
)
|
|
|
|
// Modbus is an api.Meter implementation with configurable getters and setters.
|
|
type Modbus struct {
|
|
log *util.Logger
|
|
conn meters.Connection
|
|
device meters.Device
|
|
slaveID uint8
|
|
opPower rs485.Operation
|
|
opEnergy rs485.Operation
|
|
}
|
|
|
|
// NewModbusFromConfig creates api.Meter from config
|
|
func NewModbusFromConfig(log *util.Logger, other map[string]interface{}) api.Meter {
|
|
cc := struct {
|
|
provider.ModbusSettings `mapstructure:",squash"`
|
|
Power, Energy string
|
|
}{}
|
|
util.DecodeOther(log, other, &cc)
|
|
|
|
// assume RTU if not set and this is a known RS485 meter model
|
|
if cc.RTU == nil {
|
|
b := modbus.IsRS485(cc.Model)
|
|
cc.RTU = &b
|
|
}
|
|
|
|
conn := modbus.NewConnection(log, cc.URI, cc.Device, cc.Comset, cc.Baudrate, *cc.RTU)
|
|
device, err := modbus.NewDevice(log, cc.Model, *cc.RTU)
|
|
|
|
log = util.NewLogger("modb")
|
|
conn.Logger(log.TRACE)
|
|
|
|
// prepare device
|
|
if err == nil {
|
|
conn.Slave(cc.ID)
|
|
err = device.Initialize(conn.ModbusClient())
|
|
|
|
// silence Kostal implementation errors
|
|
if _, partial := err.(meters.SunSpecPartiallyInitialized); partial {
|
|
err = nil
|
|
}
|
|
}
|
|
if err != nil {
|
|
log.FATAL.Fatal(err)
|
|
}
|
|
|
|
m := &Modbus{
|
|
log: log,
|
|
conn: conn,
|
|
device: device,
|
|
slaveID: cc.ID,
|
|
}
|
|
|
|
// power reading
|
|
if cc.Power == "" {
|
|
cc.Power = "Power"
|
|
}
|
|
|
|
powerM, err := meters.MeasurementString(cc.Power)
|
|
if err != nil {
|
|
log.FATAL.Fatalf("invalid measurement for power: %s", cc.Power)
|
|
}
|
|
|
|
// for RS485 check if producer supports the measurement
|
|
m.opPower = rs485.Operation{IEC61850: powerM}
|
|
if dev, ok := device.(*rs485.RS485); ok {
|
|
m.opPower = modbus.RS485FindDeviceOp(dev, powerM)
|
|
|
|
if m.opPower.IEC61850 == 0 {
|
|
log.FATAL.Fatalf("unsupported measurement for power: %s", cc.Power)
|
|
}
|
|
}
|
|
|
|
// decorate energy reading
|
|
if cc.Energy != "" {
|
|
energyM, err := meters.MeasurementString(cc.Energy)
|
|
if err != nil {
|
|
log.FATAL.Fatalf("invalid measurement for energy: %s", cc.Energy)
|
|
}
|
|
|
|
// for RS485 check if producer supports the measurement
|
|
m.opEnergy = rs485.Operation{IEC61850: energyM}
|
|
if dev, ok := device.(*rs485.RS485); ok {
|
|
m.opEnergy = modbus.RS485FindDeviceOp(dev, energyM)
|
|
|
|
if m.opEnergy.IEC61850 == 0 {
|
|
log.FATAL.Fatalf("unsupported measurement for energy: %s", cc.Energy)
|
|
}
|
|
}
|
|
|
|
return &ModbusEnergy{m}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// floatGetter executes configured modbus read operation and implements provider.FloatGetter
|
|
func (m *Modbus) floatGetter(op rs485.Operation) (float64, error) {
|
|
m.conn.Slave(m.slaveID)
|
|
|
|
var res meters.MeasurementResult
|
|
var err error
|
|
|
|
if dev, ok := m.device.(*rs485.RS485); ok {
|
|
res, err = dev.QueryOp(m.conn.ModbusClient(), op)
|
|
}
|
|
|
|
if dev, ok := m.device.(*sunspec.SunSpec); ok {
|
|
res, err = dev.QueryOp(m.conn.ModbusClient(), op.IEC61850)
|
|
}
|
|
|
|
if err == nil {
|
|
m.log.TRACE.Printf("%+v", res)
|
|
}
|
|
|
|
return res.Value, err
|
|
}
|
|
|
|
// CurrentPower implements the Meter.CurrentPower interface
|
|
func (m *Modbus) CurrentPower() (float64, error) {
|
|
return m.floatGetter(m.opPower)
|
|
}
|
|
|
|
// ModbusEnergy decorates Modbus with api.MeterEnergy interface
|
|
type ModbusEnergy struct {
|
|
*Modbus
|
|
}
|
|
|
|
// TotalEnergy implements the Meter.TotalEnergy interface
|
|
func (m *ModbusEnergy) TotalEnergy() (float64, error) {
|
|
return m.floatGetter(m.opEnergy)
|
|
}
|