diff --git a/.gitignore b/.gitignore index 4c8c71c..d921623 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ -/sofar -sofar_g3_lsw3_logger_reader config.yaml -sofar +sofar-x86 sofar-arm -.idea \ No newline at end of file +.vscode +.idea diff --git a/Makefile b/Makefile index c3bf0d3..c75cc2f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ +all: build-arm build-x86 + build-arm: - env GOOS=linux GOARCH=arm GOARM=5 go build -o sofar-arm + env GOOS=linux GOARCH=arm GOARM=5 go build -o custom_components/sofar_g3_lsw3_logger_reader/sofar-arm -build: - go build -o sofar \ No newline at end of file +build-x86: + go build -o custom_components/sofar_g3_lsw3_logger_reader/sofar-x86 \ No newline at end of file diff --git a/adapters/comms/tcpip/tcpip.go b/adapters/comms/tcpip/tcpip.go index 9206aa9..6906302 100644 --- a/adapters/comms/tcpip/tcpip.go +++ b/adapters/comms/tcpip/tcpip.go @@ -1,7 +1,6 @@ package tcpip import ( - "bufio" "fmt" "net" "time" @@ -51,13 +50,11 @@ func (s *tcpIpPort) Read(buf []byte) (int, error) { return 0, fmt.Errorf("connection is not open") } - reader := bufio.NewReader(s.conn) - if err := s.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { return 0, err } - return reader.Read(buf) + return s.conn.Read(buf) } func (s *tcpIpPort) Write(payload []byte) (int, error) { diff --git a/adapters/devices/sofar/device.go b/adapters/devices/sofar/device.go index 2671919..89c3976 100644 --- a/adapters/devices/sofar/device.go +++ b/adapters/devices/sofar/device.go @@ -1,21 +1,71 @@ package sofar -import "github.com/kubaceg/sofar_g3_lsw3_logger_reader/ports" +import ( + "log" + "regexp" + + "github.com/kubaceg/sofar_g3_lsw3_logger_reader/ports" +) type Logger struct { - serialNumber uint - connPort ports.CommunicationPort + serialNumber uint + connPort ports.CommunicationPort + attrWhiteList map[string]struct{} + attrBlackList []*regexp.Regexp +} + +// for a set in go we use a map of keys -> empty struct +func toSet(slice []string) map[string]struct{} { + set := make(map[string]struct{}, len(slice)) + v := struct{}{} + for _, s := range slice { + set[s] = v + } + return set +} + +func toREs(patterns []string) []*regexp.Regexp { + res := make([]*regexp.Regexp, 0, len(patterns)) + for idx, p := range patterns { + re, err := regexp.Compile(p) + if err == nil { + res = append(res, re) + } else { + log.Printf("config attrBlackList item %d '%s' not a valid regexp; %v", idx, p, err) + } + } + return res } -func NewSofarLogger(serialNumber uint, connPort ports.CommunicationPort) *Logger { +func NewSofarLogger(serialNumber uint, connPort ports.CommunicationPort, attrWhiteList []string, attrBlackList []string) *Logger { return &Logger{ - serialNumber: serialNumber, - connPort: connPort, + serialNumber: serialNumber, + connPort: connPort, + attrWhiteList: toSet(attrWhiteList), + attrBlackList: toREs(attrBlackList), + } +} + +func (s *Logger) nameFilter(k string) bool { + if len(s.attrWhiteList) > 0 { + _, ok := s.attrWhiteList[k] + return ok + } else { + for _, re := range s.attrBlackList { + if re.MatchString(k) { + return false + } + } } + return true +} + +func (s *Logger) GetDiscoveryFields() []ports.DiscoveryField { + return getDiscoveryFields(s.nameFilter) } func (s *Logger) Query() (map[string]interface{}, error) { - return readData(s.connPort, s.serialNumber) + return readData(s.connPort, s.serialNumber, s.nameFilter) } func (s *Logger) Name() string { diff --git a/adapters/devices/sofar/lsw.go b/adapters/devices/sofar/lsw.go index b8246a1..f04a790 100644 --- a/adapters/devices/sofar/lsw.go +++ b/adapters/devices/sofar/lsw.go @@ -73,7 +73,7 @@ func (l LSWRequest) checksum(buf []byte) uint8 { return checksum } -func readData(connPort ports.CommunicationPort, serialNumber uint) (map[string]interface{}, error) { +func readData(connPort ports.CommunicationPort, serialNumber uint, nameFilter func(string) bool) (map[string]interface{}, error) { result := make(map[string]interface{}) for _, rr := range allRegisterRanges { @@ -81,9 +81,10 @@ func readData(connPort ports.CommunicationPort, serialNumber uint) (map[string]i if err != nil { return nil, err } - for k, v := range reply { - result[k] = v + if nameFilter(k) { + result[k] = v + } } } return result, nil @@ -111,27 +112,25 @@ func readRegisterRange(rr registerRange, connPort ports.CommunicationPort, seria return nil, err } - // read the result - buf := make([]byte, 2048) - n, err := connPort.Read(buf) - if err != nil { - return nil, err - } - - // truncate the buffer - buf = buf[:n] - if len(buf) < 48 { - // short reply - return nil, fmt.Errorf("short reply: %d bytes", n) + // read enough bytes + buf := []byte{} + for { + b := make([]byte, 2048) + n, err := connPort.Read(b) + if n > 0 { + buf = append(buf, b[:n]...) + } + if err != nil { + return nil, err + } + if len(buf) >= 28 && len(buf) >= 28+int(buf[27]) { + break + } } - - replyBytesCount := buf[27] - - modbusReply := buf[28 : 28+replyBytesCount] + modbusReply := buf[28 : 28+buf[27]] // shove the data into the reply reply := make(map[string]interface{}) - for _, f := range rr.replyFields { fieldOffset := (f.register - rr.start) * 2 diff --git a/adapters/devices/sofar/sofar_protocol.go b/adapters/devices/sofar/sofar_protocol.go index 369fd34..0ccb751 100644 --- a/adapters/devices/sofar/sofar_protocol.go +++ b/adapters/devices/sofar/sofar_protocol.go @@ -1,5 +1,9 @@ package sofar +import ( + "github.com/kubaceg/sofar_g3_lsw3_logger_reader/ports" +) + type field struct { register int name string @@ -37,6 +41,18 @@ func GetAllRegisterNames() []string { return result } +func getDiscoveryFields(nameFilter func(string) bool) []ports.DiscoveryField { + result := make([]ports.DiscoveryField, 0) + for _, rr := range allRegisterRanges { + for _, f := range rr.replyFields { + if f.name != "" && f.valueType != "" && nameFilter(f.name) { + result = append(result, ports.DiscoveryField{Name: f.name, Factor: f.factor, Unit: f.unit}) + } + } + } + return result +} + var rrSystemInfo = registerRange{ start: 0x400, end: 0x43a, @@ -75,18 +91,18 @@ var rrSystemInfo = registerRange{ {0x0423, "Temp_Rsvd1", "I16", "1", "℃"}, {0x0424, "Temp_Rsvd2", "I16", "1", "℃"}, {0x0425, "Temp_Rsvd3", "I16", "1", "℃"}, - {0x0426, "GenerationTime_Today", "U16", "1", "Minute"}, - {0x0427, "GenerationTime_Total", "U32", "1", "Minute"}, + {0x0426, "GenerationTime_Today", "U16", "1", "min"}, // HA uses d, h, min, s not Minute + {0x0427, "GenerationTime_Total", "U32", "1", "min"}, {0x0428, "", "", "", ""}, - {0x0429, "ServiceTime_Total", "U32", "1", "Minute"}, + {0x0429, "ServiceTime_Total", "U32", "1", "min"}, {0x042A, "", "", "", ""}, {0x042B, "InsulationResistance", "U16", "1", "kΩ"}, {0x042C, "SysTime_Year", "U16", "", ""}, {0x042D, "SysTime_Month", "U16", "", ""}, - {0x042E, "SysTime_Date", "U16", "", ""}, - {0x042F, "SysTime_Hour", "U16", "", ""}, - {0x0430, "SysTime_Minute", "U16", "", ""}, - {0x0431, "SysTime_Second", "U16", "", ""}, + {0x042E, "SysTime_Date", "U16", "1", "d"}, + {0x042F, "SysTime_Hour", "U16", "1", "h"}, + {0x0430, "SysTime_Minute", "U16", "1", "min"}, + {0x0431, "SysTime_Second", "U16", "1", "s"}, {0x0432, "Fault19", "U16", "", ""}, {0x0433, "Fault20", "U16", "", ""}, {0x0434, "Fault21", "U16", "", ""}, @@ -104,11 +120,11 @@ var rrEnergyTodayTotals = registerRange{ replyFields: []field{ {0x684, "PV_Generation_Today", "U32", "0.01", "kWh"}, {0x686, "PV_Generation_Total", "U32", "0.1", "kWh"}, - {0x688, "Load_Consumption_Today", "U32", "0.1", "kWh"}, + {0x688, "Load_Consumption_Today", "U32", "0.01", "kWh"}, {0x68A, "Load_Consumption_Total", "U32", "0.1", "kWh"}, - {0x68C, "Energy_Purchase_Today", "U32", "0.1", "kWh"}, + {0x68C, "Energy_Purchase_Today", "U32", "0.01", "kWh"}, {0x68E, "Energy_Purchase_Total", "U32", "0.1", "kWh"}, - {0x690, "Energy_Selling_Today", "U32", "0.1", "kWh"}, + {0x690, "Energy_Selling_Today", "U32", "0.01", "kWh"}, {0x692, "Energy_Selling_Total", "U32", "0.1", "kWh"}, {0x694, "Bat_Charge_Today", "U32", "0.01", "kWh"}, {0x696, "Bat_Charge_Total", "U32", "0.1", "kWh"}, diff --git a/adapters/export/mosquitto/mqtt.go b/adapters/export/mosquitto/mqtt.go index 80305ce..9beb630 100644 --- a/adapters/export/mosquitto/mqtt.go +++ b/adapters/export/mosquitto/mqtt.go @@ -4,21 +4,24 @@ import ( "encoding/json" "fmt" "log" + "strings" "time" mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/kubaceg/sofar_g3_lsw3_logger_reader/ports" ) type MqttConfig struct { - Url string `yaml:"url"` - User string `yaml:"user"` - Password string `yaml:"password"` - Prefix string `yaml:"prefix"` + Url string `yaml:"url"` + User string `yaml:"user"` + Password string `yaml:"password"` + Discovery string `yaml:"discovery"` + State string `yaml:"state"` } type Connection struct { client mqtt.Client - prefix string + state string } var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) { @@ -46,7 +49,7 @@ func New(config *MqttConfig) (*Connection, error) { conn := &Connection{} conn.client = mqtt.NewClient(opts) - conn.prefix = config.Prefix + conn.state = config.State if token := conn.client.Connect(); token.Wait() && token.Error() != nil { return nil, token.Error() } @@ -55,27 +58,77 @@ func New(config *MqttConfig) (*Connection, error) { } -func (conn *Connection) InsertRecord(measurement map[string]interface{}) error { - measurementCopy := make(map[string]interface{}, len(measurement)) - for k, v := range measurement { - measurementCopy[k] = v +func (conn *Connection) publish(topic string, msg string, retain bool) { + token := conn.client.Publish(topic, 0, retain, msg) + res := token.WaitTimeout(1 * time.Second) + if !res || token.Error() != nil { + log.Printf("error inserting to MQTT: %s", token.Error()) } - go func(measurement map[string]interface{}) { - // timestamp it - measurement["LastTimestamp"] = time.Now().UnixNano() / int64(time.Millisecond) - m, _ := json.Marshal(measurement) - measurement["All"] = string(m) - - for k, v := range measurement { - token := conn.client.Publish(fmt.Sprintf("%s/%s", conn.prefix, k), 0, true, fmt.Sprintf("%v", v)) - res := token.WaitTimeout(1 * time.Second) - if !res || token.Error() != nil { - log.Printf("error inserting to MQTT: %s", token.Error()) - } - } - - }(measurementCopy) +} + +// return "power" for kW etc., "energy" for kWh etc. +func unit2DeviceClass(unit string) string { + if strings.HasSuffix(unit, "Wh") { + return "energy" + } else if strings.HasSuffix(unit, "W") { + return "power" + } else if strings.HasSuffix(unit, "Hz") { + return "frequency" + } else if strings.HasSuffix(unit, "VA") { + return "apparent_power" + } else if strings.HasSuffix(unit, "VAR") { + return "reactive_power" + } else if strings.HasSuffix(unit, "V") { + return "voltage" + } else if strings.HasSuffix(unit, "A") { + return "current" + } else if strings.HasSuffix(unit, "Ω") { + return "voltage" // resistance not valid in https://developers.home-assistant.io/docs/core/entity/sensor/ so use "voltage" + } else if strings.HasSuffix(unit, "℃") { + return "temperature" + } else if strings.HasSuffix(unit, "min") { + return "duration" + } else { + return "" + } +} + +func unit2StateClass(unit string) string { + if strings.HasSuffix(unit, "Wh") { + return "total" + } else { + return "measurement" + } +} + +// MQTT Discovery: https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery +func (conn *Connection) InsertDiscoveryRecord(discovery string, state string, expireAfter int, fields []ports.DiscoveryField) error { + uniq := "01ad" // TODO: get from config? + for _, f := range fields { + topic := fmt.Sprintf("%s/%s/config", discovery, f.Name) + json, _ := json.Marshal(map[string]interface{}{ + "name": f.Name, + "unique_id": fmt.Sprintf("%s_%s", f.Name, uniq), + "device_class": unit2DeviceClass(f.Unit), + "state_class": unit2StateClass(f.Unit), + "state_topic": state, + "unit_of_measurement": f.Unit, + "value_template": fmt.Sprintf("{{ value_json.%s|int * %s }}", f.Name, f.Factor), + "availability_topic": state, + "availability_template": "{{ value_json.availability }}", + "device": map[string]interface{}{ + "identifiers": [...]string{fmt.Sprintf("Inverter_%s", uniq)}, + "name": "Inverter", + }, + }) + conn.publish(topic, string(json), true) // MQTT Discovery messages should be retained, but in dev it can become a pain + } + return nil +} +func (conn *Connection) InsertRecord(m map[string]interface{}) error { + json, _ := json.Marshal(m) + conn.publish(conn.state, string(json), false) // state messages should not be retained return nil } diff --git a/config-example.yaml b/config-example.yaml index 1ac32e9..f26b02c 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -1,15 +1,90 @@ +# make your own in config.yaml + inverter: - port: 1.2.3.4:23 # required port name (e.g. /dev/ttyUSB0 for serial or 1.2.3.4:23 for TCP/IP - loggerSerial: 123456 # required logger serial number - readInterval: 60 # update interval in seconds, default 60 -mqtt: - url: 1.2.3.4:5678 #MQTT broker URL (e.g. 1.2.3.4:5678) - user: #MQTT username (leave empty when not needed) - password: #MQTT password (leave empty when not needed) - prefix: /sensors/energy/inverter #topic prefix on which data will be sent -otlp: + port: 1.2.3.4:8899 # required, port name e.g. /dev/ttyUSB0 for serial or 1.2.3.4:8899 for TCP/IP + loggerSerial: 23XXXXXXXX # required, logger serial number + readInterval: 10 # update interval in seconds, default 60 + loopLogging: true # false to avoid a line to log file every readInterval + # + # if there is a non-empty attrWhiteList then only these explicitly listed attributes are output + attrWhiteList: + - ActivePower_Output_Total # Total PV generation (in units of 10W) + - ActivePower_Load_Sys # total power consumption (10W) + - ActivePower_PCC_Total # grid export (+) or import (-) (10W) = ActivePower_Output_Total - ActivePower_Load_Sys (so it is redundant data) + - Power_PV1 # PV output of string 1 (10W) + - Power_PV2 # PV output of string 2 (10W); PV1 + PV2 > ActivePower_Output_Total by about 3.5% due to inverter inefficiency? + - PV_Generation_Today # generation since midnight (10Wh) + - PV_Generation_Total + - GenerationTime_Today + - GenerationTime_Total + - Frequency_Grid + - Voltage_Phase_R # grid voltage on phase R/1 + - InsulationResistance # changes to these last ones might indicate a problem + - Temperature_Env1 + - Temperature_HeatSink1 + # else attributes are output unless they match a regex in attrBlackList + attrBlackList: + - "^[ST]_" # prefix R_, S_, T_ for 3 phases, only R_ used in single phase systems + - "_[ST]$" # likewise suffixes _R, _S, _T + +mqtt: # MQTT disabled if url & prefix both blank + url: 1.2.3.4:1883 # MQTT broker URL (e.g. 1.2.3.4:1883) + user: # MQTT username (leave empty when not needed) + password: # MQTT password (leave empty when not needed) + discovery: homeassistant/sensor # topic prefix for MQTT Discovery + state: energy/inverter/state # topic for state + +otlp: # OTLP disabled if both urls blank grpc: - url: 0.0.0.0:4317 #URL for gRPC OTLP server + url: # URL for gRPC OTLP server e.g. 0.0.0.0:4317 http: - url: 0.0.0.0:4318 #URL for HTTP OLTP server + url: # URL for HTTP OLTP server e.g. 0.0.0.0:4318 prefix: sofar.logging + +# List of all attributes with non-zero values fetched from my inverter: +# +# # Most interesting to me: +# "ActivePower_Load_Sys": 16, # total power consumption (10W) +# "ActivePower_Output_Total": 333, # Total PV generation (in units of 10W) +# "ActivePower_PCC_Total": 317, # grid export (+) or import (-) (10W) = ActivePower_Output_Total - ActivePower_Load_Sys (so it is redundant data) +# "GenerationTime_Today": 553, +# "GenerationTime_Total": 54992, +# "PV_Generation_Today": 3428, # generation since midnight (10Wh) +# "PV_Generation_Total": 20355, + +# # Not credible: +# "Load_Consumption_Today": 3428, # same as PV_Generation_Today, so not credible as Load +# "Load_Consumption_Total": 20355, # same as PV_Generation_Total, so not credible as Load + +# # Inverter: +# "ApparentPower_Output_Total": 33, +# "Countdown": 60, +# "InsulationResistance": 1437, +# "ServiceTime_Total": 56198, +# "SysState": 2, +# "SysTime_Date": 19, +# "SysTime_Hour": 14, +# "SysTime_Minute": 56, +# "SysTime_Month": 9, +# "SysTime_Second": 1, +# "SysTime_Year": 23, +# "Temperature_Env1": 48, # Single Plate Ambient Temperature (label from SolarMan app) +# "Temperature_HeatSink1": 35, # Radiator Temperature 1 (label from SolarMan app) +# "Temperature_HeatSink2": 120, # value never seems to change + +# # PV strings of panels: +# "Power_PV1": 175, # PV output of string 1 (10W) +# "Power_PV2": 172, # PV output of string 2 (10W); PV1 + PV2 > ActivePower_Output_Total by about 3.5% due to inverter inefficiency? +# "Voltage_PV1": 2438, +# "Voltage_PV2": 2436, +# "Current_PV1": 720, +# "Current_PV2": 706, + +# # Grid: +# "Frequency_Grid": 5000, + +# # Phase R (single phase power): +# "ActivePower_PCC_R": 317, # same as ActivePower_PCC_Total +# "Voltage_Phase_R": 2352 # grid voltage +# "Current_Output_R": 1418, +# "Current_PCC_R": 1394, diff --git a/config.go b/config.go index 424e399..225f15a 100644 --- a/config.go +++ b/config.go @@ -2,18 +2,22 @@ package main import ( "errors" - "github.com/kubaceg/sofar_g3_lsw3_logger_reader/adapters/export/otlp" "os" + "github.com/kubaceg/sofar_g3_lsw3_logger_reader/adapters/export/otlp" + "github.com/kubaceg/sofar_g3_lsw3_logger_reader/adapters/export/mosquitto" "gopkg.in/yaml.v2" ) type Config struct { Inverter struct { - Port string `yaml:"port"` - LoggerSerial uint `yaml:"loggerSerial"` - ReadInterval int `default:"60" yaml:"readInterval"` + Port string `yaml:"port"` + LoggerSerial uint `yaml:"loggerSerial"` + ReadInterval int `default:"60" yaml:"readInterval"` + LoopLogging bool `default:"true" yaml:"loopLogging"` + AttrWhiteList []string `yaml:"attrWhiteList"` + AttrBlackList []string `yaml:"attrBlackList"` } `yaml:"inverter"` Mqtt mosquitto.MqttConfig `yaml:"mqtt"` Otlp otlp.Config `yaml:"otlp"` diff --git a/go.mod b/go.mod index 489087b..baf3b21 100644 --- a/go.mod +++ b/go.mod @@ -20,17 +20,19 @@ require ( github.com/creack/goselect v0.1.2 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.37.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.53.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect + google.golang.org/grpc v1.58.2 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 53c4569..a8a4ea0 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -57,6 +59,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/eclipse/paho.mqtt.golang v1.3.1 h1:6F5FYb1hxVSZS+p0ji5xBQamc5ltOolTYRy5R15uVmI= github.com/eclipse/paho.mqtt.golang v1.3.1/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -71,6 +75,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -103,6 +109,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -130,9 +138,15 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -144,6 +158,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -157,11 +172,14 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.bug.st/serial v1.4.0 h1:IXHzPVbUBbql66lQZ1iV9LWzGXT5lh6S9gZxHK/KyQE= go.bug.st/serial v1.4.0/go.mod h1:z8CesKorE90Qr/oRSJiEuvzYRKol9r/anJZEb5kt304= +go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY= +go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -169,25 +187,45 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 h1:/fXHZHGvro6MVqV34fJzDhi7sHGpX3Ej/Qjmfn003ho= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0/go.mod h1:UFG7EBMRdXyFstOwH028U0sVf+AvukSGhF0g8+dmNG8= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.17.0 h1:eU0ffpYuEY7eQ75K+nKr9CI5KcY8h+GPk/9DDlEO1NI= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.17.0/go.mod h1:9P5RK5JS2sjKepuCkqFwPp3etwV/57E0eigLw18Mn1k= go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.37.0 h1:22J9c9mxNAZugv86zhwjBnER0DbO0VVpW9Oo/j3jBBQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.37.0/go.mod h1:QD8SSO9fgtBOvXYpcX5NXW+YnDJByTnh7a/9enQWFmw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 h1:ZtfnDL+tUrs1F0Pzfwbg2d59Gru9NCH3bgSHBM6LDwU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.37.0 h1:CI6DSdsSkJxX1rsfPSQ0SciKx6klhdDRBXqKb+FwXG8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.37.0/go.mod h1:WLBYPrz8srktckhCjFaau4VHSfGaMuqoKSXwpzaiRZg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 h1:NmnYCiR0qNufkldjVvyQfZTHSdzeHoZ41zggMsdMcLM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0/go.mod h1:UVAO61+umUsHLtYb8KXXRoHtxUkdOPkYidzW3gipRLQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.37.0 h1:Ad4fpLq5t4s4+xB0chYBmbp1NNMqG4QRkseRmbx3bOw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.37.0/go.mod h1:hgpB6JpYB/K403Z2wCxtX5fENB1D4bSdAHG0vJI+Koc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 h1:wNMDy/LVGLj2h3p6zg4d0gypKfWKSWI14E1C4smOgl8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0/go.mod h1:YfbDdXAAkemWJK3H/DshvlrxqFB2rtW4rY6ky/3x/H0= go.opentelemetry.io/otel/metric v0.37.0 h1:pHDQuLQOZwYD+Km0eb657A25NaRzy0a+eLyKfDXedEs= go.opentelemetry.io/otel/metric v0.37.0/go.mod h1:DmdaHfGt54iV6UKxsV9slj2bBRJcKC1B1uvDLIioc1s= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk/metric v0.37.0 h1:haYBBtZZxiI3ROwSmkZnI+d0+AVzBWeviuYQDeBWosU= go.opentelemetry.io/otel/sdk/metric v0.37.0/go.mod h1:mO2WV1AZKKwhwHTV3AKOoIEb9LbUaENZDuGUQd+j4A0= +go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k= +go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY= go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -203,6 +241,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -253,6 +293,12 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -267,6 +313,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -297,6 +347,13 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -306,6 +363,12 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -408,6 +471,22 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk= +google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8= +google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:EMfReVxb80Dq1hhioy0sOsY9jCE46YDgHlJ7fWVUWRE= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0= +google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a h1:myvhA4is3vrit1a6NZCWBIwN0kNEnX21DJOJX/NvIfI= +google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:SUBoKXbI1Efip18FClrQVGjWcyd0QZd8KkvdP34t7ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -426,6 +505,11 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -441,6 +525,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..9e72c0f --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "Sofar LSW3", + "homeassistant": "0.116.0" +} diff --git a/info.md b/info.md new file mode 100644 index 0000000..f9b61ae --- /dev/null +++ b/info.md @@ -0,0 +1,29 @@ +# Integration for Sofar Solar PV Inverters with LSW-3 WiFi data logger with SN 23XXXXXXX + +This integration regularly polls the LSW-3 and sends inverter data on a MQTT topic. +It supports MQTT discovery. +It is implemented by a binary executable `sofar-arm` (on arm) or `sofar-x86` (on x86), which is kicked off when the integration starts and runs forever. + +## Manual Installation + +1. First get the go program `sofar_g3_lsw3_logger_reader` working on your dev machine, so that you have a working `config.yaml` and `sofar-x86` (x86) and `sofar-arm` (arm) binary executables. +2. Copy this directory `custom_components/sofar_g3_lsw3_logger_reader` to homeassistant's `/config/custom_components` +(creating `/config/custom_components/sofar_g3_lsw3_logger_reader`). +You can use the `Samba share` or `Advanced SSH & Web Terminal` add-ons to do this. +3. Copy the working `config.yaml` (from step 1) to the same directory. +4. Copy the executables `sofar` (x86) and `sofar-arm` (arm) (from step 1) to this same directory. +5. Enable the custom component by adding a line `sofar_g3_lsw3_logger_reader:` to homeassistant's `/config/configuration.yaml`. +6. Do a full restart of homeassistant: `Developer Tools` > `YAML` > `CHECK CONFIGURATION` then `RESTART` > `Restart Home Assistant` +7. Check the content of `err.log` in this same directory. +8. Add the `Inverter` device to your dashhboard: `Settings` > `Devices & Services` > `Integration` > `MQTT` > `1 device` > `ADD TO DASHBOARD` + +## HACS Installation + +1. Install using HACS +2. In directory `/config/custom_components/sofar_g3_lsw3_logger_reader` copy `config-example.yaml` to `config.yaml` and edit to match your requirements. +3. Restart Home Assistant to run the integration. + +## To Do + +1. Get logging going to homeassistant's log. +2. Add support for "config flow" diff --git a/main.go b/main.go index c475640..693a964 100644 --- a/main.go +++ b/main.go @@ -14,12 +14,9 @@ import ( "github.com/kubaceg/sofar_g3_lsw3_logger_reader/adapters/devices/sofar" "github.com/kubaceg/sofar_g3_lsw3_logger_reader/adapters/export/mosquitto" "github.com/kubaceg/sofar_g3_lsw3_logger_reader/adapters/export/otlp" - "github.com/kubaceg/sofar_g3_lsw3_logger_reader/ports" ) -// maximumFailedConnections maximum number failed logger connection, after this number will be exceeded reconnect -// interval will be extended from 5s to readInterval defined in config file const maximumFailedConnections = 3 var ( @@ -40,7 +37,7 @@ func initialize() { log.Fatalln(err) } - hasMQTT = config.Mqtt.Url != "" && config.Mqtt.Prefix != "" + hasMQTT = config.Mqtt.Url != "" && config.Mqtt.State != "" hasOTLP = config.Otlp.Grpc.Url != "" || config.Otlp.Http.Url != "" if isSerialPort(config.Inverter.Port) { @@ -66,61 +63,68 @@ func initialize() { } } - device = sofar.NewSofarLogger(config.Inverter.LoggerSerial, port) + device = sofar.NewSofarLogger(config.Inverter.LoggerSerial, port, config.Inverter.AttrWhiteList, config.Inverter.AttrBlackList) } func main() { initialize() - failedConnections := 0 - - for { - log.Printf("performing measurements") - timeStart := time.Now() - measurements, err := device.Query() + if hasMQTT { + err := mqtt.InsertDiscoveryRecord(config.Mqtt.Discovery, config.Mqtt.State, config.Inverter.ReadInterval*5, device.GetDiscoveryFields()) // logs errors, always returns nil if err != nil { - log.Printf("failed to perform measurements: %s", err) - failedConnections++ - - if failedConnections > maximumFailedConnections { - time.Sleep(time.Duration(config.Inverter.ReadInterval) * time.Second) - } + log.Printf("never happens: %s", err) + } + } - continue + for { + if config.Inverter.LoopLogging { + log.Printf("performing measurements") } - failedConnections = 0 + var measurements map[string]interface{} = nil + var err error + for retry := 0; measurements == nil && retry < maximumFailedConnections; retry++ { + measurements, err = device.Query() + if err != nil { + log.Printf("failed to perform measurements on retry %d: %s", retry, err) + // at night, inverter is offline, err = "dial tcp 192.168.xx.xxx:8899: i/o timeout" + // at other times occaisionally: "read tcp 192.168.68.104:38670->192.168.68.106:8899: i/o timeout" + } + } if hasMQTT { - go func() { - err = mqtt.InsertRecord(measurements) - if err != nil { - log.Printf("failed to insert record to MQTT: %s\n", err) - } else { - log.Println("measurements pushed to MQTT") + var m map[string]interface{} + timeStamp := time.Now().UnixNano() / int64(time.Millisecond) + if measurements != nil { + m = make(map[string]interface{}, len(measurements)+2) + for k, v := range measurements { + m[k] = v } - }() - } - - if hasOTLP { - go func() { - err = telem.CollectAndPushMetrics(context.Background(), measurements) - if err != nil { - log.Printf("error recording telemetry: %s\n", err) - } else { - log.Println("measurements pushed via OLTP") + m["availability"] = "online" + m["LastTimestamp"] = timeStamp + } else { + m = map[string]interface{}{ + "availability": "offline", + "LastTimestamp": timeStamp, } - }() + } + err := mqtt.InsertRecord(m) // logs errors, always returns nil + if err != nil { + log.Printf("never happens: %s", err) + } } - duration := time.Since(timeStart) + if hasOTLP && measurements != nil { + err := telem.CollectAndPushMetrics(context.Background(), measurements) + if err != nil { + log.Printf("error recording telemetry: %s\n", err) + } else { + log.Println("measurements pushed via OLTP") + } - delay := time.Duration(config.Inverter.ReadInterval)*time.Second - duration - if delay <= 0 { - delay = 1 * time.Second } - time.Sleep(delay) + time.Sleep(time.Duration(config.Inverter.ReadInterval) * time.Second) } } diff --git a/ports/database.go b/ports/database.go index 8f5557c..cf88106 100644 --- a/ports/database.go +++ b/ports/database.go @@ -3,6 +3,7 @@ package ports import mqtt "github.com/eclipse/paho.mqtt.golang" type Database interface { + InsertDiscoveryRecord(discovery string, prefix string, expireAfter int, fields []DiscoveryField) error InsertRecord(measurement map[string]interface{}) error } diff --git a/ports/devices.go b/ports/devices.go index 6a8821d..566b9b7 100644 --- a/ports/devices.go +++ b/ports/devices.go @@ -1,6 +1,14 @@ package ports +// support MQTT Discovery +type DiscoveryField struct { + Name string + Factor string + Unit string +} + type Device interface { Name() string + GetDiscoveryFields() []DiscoveryField Query() (map[string]interface{}, error) } diff --git a/sh/cleanmqtt.sh b/sh/cleanmqtt.sh new file mode 100644 index 0000000..702a8d5 --- /dev/null +++ b/sh/cleanmqtt.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# delete all "retained" messages +echo "cleaning " $1 " :: usage: cleanmqtt " +host="$1" +# get your MQTT credentials from HA UI: Settings > Devices & Services > MQTT > Configure > Reconfigure MQTT +user=homeassistant +paswd="yourPassword" + +mosquitto_sub -h "$host" -u "$user" -P "$paswd" -t "#" -v --retained-only | while read topic value +do + echo "cleaning topic $topic" + mosquitto_pub -h "$host" -u "$user" -P "$paswd" -t "$topic" -r -n +done