package detect import ( "encoding/binary" "errors" "fmt" "time" "github.com/andig/evcc/util" "github.com/andig/evcc/util/modbus" gridx "github.com/grid-x/modbus" "github.com/volkszaehler/mbmd/meters" "github.com/volkszaehler/mbmd/meters/rs485" "github.com/volkszaehler/mbmd/meters/sunspec" ) func init() { registry.Add("modbus", ModbusHandlerFactory) } type ModbusResult struct { SlaveID uint8 Model int Point string Value interface{} } func (r *ModbusResult) Configuration(handler TaskHandler, res Result) map[string]interface{} { port := handler.(*ModbusHandler).Port cc := map[string]interface{}{ "uri": fmt.Sprintf("%s:%d", res.Host, port), "model": "sunspec", "id": r.SlaveID, } return cc } func ModbusHandlerFactory(conf map[string]interface{}) (TaskHandler, error) { handler := ModbusHandler{ Port: 502, IDs: []uint8{1}, Models: []int{1}, Point: "Md", // Model Timeout: 10 * timeout, } err := util.DecodeOther(conf, &handler) if err == nil && len(handler.IDs) == 0 { err = errors.New("missing slave IDs") } if handler.Register.Address > 0 { handler.op, err = modbus.RegisterOperation(handler.Register) } return &handler, err } type ModbusHandler struct { Port int IDs []uint8 Models []int Point string Register modbus.Register `mapstructure:",squash"` Values []int Invalid []int op rs485.Operation Timeout time.Duration } func (h *ModbusHandler) testRegister(log *util.Logger, conn gridx.Client) bool { var bytes []byte var err error switch h.op.FuncCode { case rs485.ReadHoldingReg: bytes, err = conn.ReadHoldingRegisters(h.op.OpCode, h.op.ReadLen) case rs485.ReadInputReg: bytes, err = conn.ReadInputRegisters(h.op.OpCode, h.op.ReadLen) } if err != nil { return false } if len(h.Values) == 0 { return true } var u uint64 switch h.op.ReadLen { case 1: u = uint64(binary.BigEndian.Uint16(bytes)) case 2: u = uint64(binary.BigEndian.Uint32(bytes)) case 4: u = binary.BigEndian.Uint64(bytes) } for _, val := range h.Values { if u == uint64(val) { return true } } return false } func (h *ModbusHandler) testSunSpec(log *util.Logger, conn meters.Connection, dev *sunspec.SunSpec, mr *ModbusResult) bool { err := dev.Initialize(conn.ModbusClient()) if errors.Is(err, meters.ErrPartiallyOpened) { err = nil } if err != nil { return false } if len(h.Models) == 0 { return true } for _, model := range h.Models { _, res, err := dev.QueryPointAny( conn.ModbusClient(), model, 0, h.Point, ) if err == nil { mr.Model = model mr.Point = h.Point mr.Value = res.Value() log.TRACE.Printf("model %d point %s: %v", model, mr.Point, mr.Value) if len(h.Invalid) == 0 { return true } var val int switch typ := res.Type(); typ { case "int16": val = int(res.Int16()) case "uint16": val = int(res.Uint16()) case "enum16": val = int(res.Enum16()) default: panic("invalid point type: " + typ) } for _, inv := range h.Invalid { if val != inv { return true } } } else { log.DEBUG.Printf("model %d: %v", model, err) } } return false } func (h *ModbusHandler) Test(log *util.Logger, ip string) (res []interface{}) { addr := fmt.Sprintf("%s:%d", ip, h.Port) conn := meters.NewTCP(addr) dev := sunspec.NewDevice("sunspec") defer conn.Close() conn.Logger(log.TRACE) conn.Timeout(h.Timeout) for _, slaveID := range h.IDs { // grace period for id switch conn.Slave(slaveID) time.Sleep(100 * time.Millisecond) mr := ModbusResult{ SlaveID: slaveID, } var ok bool if h.op.OpCode > 0 { log.DEBUG.Printf("slave id: %d op: %v", slaveID, h.op) ok = h.testRegister(log, conn.ModbusClient()) } else { log.DEBUG.Printf("slave id: %d models: %v", slaveID, h.Models) ok = h.testSunSpec(log, conn, dev, &mr) } if ok { res = append(res, mr) } } return res }