evcc-io/plugin/modbus.go
2026-07-19 10:44:32 +00:00

308 lines
6.8 KiB
Go

package plugin
import (
"bytes"
"context"
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
gridx "github.com/grid-x/modbus"
)
// modbusBlockTTL dedups block reads within a poll cycle, short enough to force
// a fresh read on the next cycle.
const modbusBlockTTL = time.Second
// modbusBlockCache lets sources covering the same (device, block) share one
// read per poll cycle.
var modbusBlockCache = modbus.NewCache(modbusBlockTTL)
// Modbus implements modbus RTU and TCP access
type Modbus struct {
log *util.Logger
conn *modbus.Connection
reg modbus.Register
block modbus.Block
scale float64
}
func init() {
registry.AddCtx("modbus", NewModbusFromConfig)
}
// NewModbusFromConfig creates Modbus plugin
func NewModbusFromConfig(ctx context.Context, other map[string]any) (Plugin, error) {
cc := struct {
modbus.Settings `mapstructure:",squash"`
Register modbus.Register
Block modbus.Block
Scale float64
Delay time.Duration
ConnectDelay time.Duration
Timeout time.Duration
}{
Scale: 1,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
modbus.Lock()
defer modbus.Unlock()
conn, err := modbus.NewConnection(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
if err != nil {
return nil, err
}
// set non-default timeout
conn.Timeout(cc.Timeout)
// set non-default delay
conn.Delay(cc.Delay)
// set non-default connect delay
conn.ConnectDelay(cc.ConnectDelay)
log := util.ContextLoggerWithDefault(ctx, util.NewLogger("modbus"))
conn.Logger(log.TRACE)
if err := cc.Register.Error(); err != nil {
return nil, err
}
if cc.Block.Count > 0 {
fc, err := cc.Register.FuncCode()
if err != nil {
return nil, err
}
if fc != gridx.FuncCodeReadHoldingRegisters && fc != gridx.FuncCodeReadInputRegisters {
return nil, errors.New("block read requires holding or input register type")
}
}
mb := &Modbus{
log: log,
conn: conn,
reg: cc.Register,
block: cc.Block,
scale: cc.Scale,
}
return mb, nil
}
func (m *Modbus) read(op modbus.RegisterOperation) ([]byte, error) {
switch op.FuncCode {
case gridx.FuncCodeReadHoldingRegisters:
return m.conn.ReadHoldingRegisters(op.Addr, op.Length)
case gridx.FuncCodeReadInputRegisters:
return m.conn.ReadInputRegisters(op.Addr, op.Length)
case gridx.FuncCodeReadCoils:
return m.conn.ReadCoils(op.Addr, op.Length)
default:
return nil, fmt.Errorf("invalid read function code: %d", op.FuncCode)
}
}
// readBytes returns the bytes for op. In block mode it fetches the enclosing
// block once (shared via modbusBlockCache) and extracts op at its offset.
func (m *Modbus) readBytes(op modbus.RegisterOperation) ([]byte, error) {
if m.block.Count == 0 {
return m.read(op)
}
blockOp := modbus.RegisterOperation{FuncCode: op.FuncCode, Addr: m.block.Register, Length: m.block.Count}
key := fmt.Sprintf("%s/%d/%d/%d", m.conn.Addr(), op.FuncCode, m.block.Register, m.block.Count)
payload, hit, err := modbusBlockCache.Fetch(key, func() ([]byte, error) {
return m.read(blockOp)
})
if err != nil {
return nil, err
}
if hit {
m.log.TRACE.Printf("block cache hit %s", key)
}
return m.block.Extract(op, payload)
}
var _ FloatGetter = (*Modbus)(nil)
// FloatGetter implements func() (float64, error)
func (m *Modbus) FloatGetter() (func() (f float64, err error), error) {
op, err := m.reg.Operation()
if err != nil {
return nil, err
}
decode, err := m.reg.DecodeFunc()
if err != nil {
return nil, err
}
return func() (float64, error) {
bytes, err := m.readBytes(op)
if err != nil {
return 0, fmt.Errorf("read failed: %w", err)
}
// a "*nan" sentinel decodes to NaN - the device reports no valid value
if res := m.scale * decode(bytes); !math.IsNaN(res) {
return res, nil
}
return 0, api.ErrNotAvailable
}, nil
}
var _ IntGetter = (*Modbus)(nil)
// IntGetter implements IntProvider
func (m *Modbus) IntGetter() (func() (int64, error), error) {
g, err := m.FloatGetter()
return func() (int64, error) {
res, err := g()
return int64(math.Round(res)), err
}, err
}
var _ StringGetter = (*Modbus)(nil)
// StringGetter implements StringProvider
func (m *Modbus) StringGetter() (func() (string, error), error) {
op, err := m.reg.Operation()
if err != nil {
return nil, err
}
return func() (string, error) {
b, err := m.readBytes(op)
if err != nil {
return "", err
}
return strings.TrimSpace(string(bytes.Trim(b, "\x00"))), nil
}, nil
}
var _ BoolGetter = (*Modbus)(nil)
// BoolGetter implements BoolProvider
func (m *Modbus) BoolGetter() (func() (bool, error), error) {
g, err := m.FloatGetter()
return func() (bool, error) {
res, err := g()
return res != 0, err
}, err
}
func (m *Modbus) writeFunc() (func(float64) error, error) {
op, err := m.reg.Operation()
if err != nil {
return nil, err
}
encode, err := m.reg.EncodeFunc()
if err != nil {
return nil, err
}
return func(val float64) error {
val *= m.scale
switch op.FuncCode {
case gridx.FuncCodeWriteSingleCoil:
var uval uint16
if val != 0 {
uval = 0xFF00
}
_, err = m.conn.WriteSingleCoil(op.Addr, uval)
return err
case gridx.FuncCodeWriteSingleRegister:
_, err = m.conn.WriteSingleRegister(op.Addr, uint16(val))
return err
case gridx.FuncCodeWriteMultipleRegisters:
b, err := encode(val)
if err == nil {
_, err = m.conn.WriteMultipleRegisters(op.Addr, op.Length, b)
}
return err
default:
return fmt.Errorf("invalid func code: %d", op.FuncCode)
}
}, nil
}
var _ FloatSetter = (*Modbus)(nil)
// FloatSetter implements FloatSetter
func (m *Modbus) FloatSetter(_ string) (func(float64) error, error) {
return m.writeFunc()
}
var _ IntSetter = (*Modbus)(nil)
// IntSetter implements IntSetter
func (m *Modbus) IntSetter(_ string) (func(int64) error, error) {
fun, err := m.writeFunc()
if err != nil {
return nil, err
}
return func(val int64) error {
return fun(float64(val))
}, nil
}
var _ BoolSetter = (*Modbus)(nil)
// BoolSetter implements BoolSetter
func (m *Modbus) BoolSetter(param string) (func(bool) error, error) {
set, err := m.IntSetter(param)
return func(val bool) error {
var ival int64
if val {
ival = 1
}
return set(ival)
}, err
}
var _ BytesSetter = (*Modbus)(nil)
// BytesSetter implements BytesSetter
func (m *Modbus) BytesSetter(_ string) (func([]byte) error, error) {
op, err := m.reg.Operation()
if err != nil {
return nil, err
}
return func(val []byte) error {
switch op.FuncCode {
case gridx.FuncCodeWriteMultipleRegisters:
_, err = m.conn.WriteMultipleRegisters(op.Addr, uint16(len(val)/2), val)
return err
default:
return fmt.Errorf("invalid func code: %d", op.FuncCode)
}
}, nil
}