-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MQTT Discovery, filtering, custom component #13
Changes from all commits
39e7187
d25f93d
735a892
711d09c
7ab7810
997f554
0788947
a2f98cb
fd12004
6620146
7639284
9560424
f167f09
fdb59ac
199c69c
b27d247
d164673
8eb66a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
/sofar | ||
sofar_g3_lsw3_logger_reader | ||
config.yaml | ||
sofar | ||
sofar-x86 | ||
sofar-arm | ||
.idea | ||
.vscode | ||
.idea | ||
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
build: | ||||||
go build -o sofar | ||||||
build-x86: | ||||||
go build -o custom_components/sofar_g3_lsw3_logger_reader/sofar-x86 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+50
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's bug. if there is no whitelist elements but there is blacklist element it should ignore elements from blacklist and pass others:
Suggested change
Here's unit test for it (fully generated by chat gpt :D )
|
||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
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 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to stay with a |
||
} | ||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expireAfter is unused |
||
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 | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.