Skip to content
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

Readme #15

Merged
merged 4 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/sofar
sofar_g3_lsw3_logger_reader
/bin
config.yaml
sofar
sofar-x86
sofar-arm
.idea
.vscode
.idea
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
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 bin/sofar-arm

build:
go build -o sofar
go build -o bin/sofar-x86

test:
go test -v ./...
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Program queries logger modbus port in infinite loop and sends data into MQTT top
4. Copy example config `cp config-example.yaml config.yaml`
5. Edit `config.yaml` in Your favorite editor, fill all required stuff
6. Build program `make build` or build for ARM machines e.g. raspberryPi `make build-arm`
7. Run `./sofar` or `sofar-arm`
7. Run `bin/sofar` or `bin/sofar-arm`

## Output data format
### MQTT
Expand All @@ -20,6 +20,9 @@ Data will be sent into MQTT topic with name `{mqttPrefix}/{fieldName}` where:
Full topic name for given example values is `/sensors/energy/inverter/PV_Generation_Today`.
Additional field is `All` which contains all measurements and their values marshalled into one json.

### Home Assistant
This tool can integrate with Home Assistant using MQTT protocol. If You want to configure MQTT along with [discovery](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery) feature, just fill 'ha_discovery_prefix' and all sensors will be configured automatically in Your Home Assistant instance.

### OTLP
Data can also be sent over OTLP protocol to a gRPC or http server. Typically, this would be received by the
[OTel-Collector](https://opentelemetry.io/docs/collector/) for further export to any required platform.
Expand Down
5 changes: 1 addition & 4 deletions adapters/comms/tcpip/tcpip.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package tcpip

import (
"bufio"
"fmt"
"net"
"time"
Expand Down Expand Up @@ -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) {
Expand Down
64 changes: 57 additions & 7 deletions adapters/devices/sofar/device.go
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 {
if _, ok := s.attrWhiteList[k]; ok {
return true
}
}
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 {
Expand Down
49 changes: 49 additions & 0 deletions adapters/devices/sofar/device_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package sofar

import (
"testing"
"regexp"
)

func TestNameFilter(t *testing.T) {
// Create a Logger instance with the desired attributes
logger := &Logger{
attrWhiteList: map[string]struct{}{
"whitelisted": {},
},
attrBlackList: []*regexp.Regexp{
regexp.MustCompile("^blacklisted"),
},
}

// Test case 1: Key in the white list
result := logger.nameFilter("whitelisted")
if result != true {
t.Errorf("Expected: true, Got: %v", result)
}

// Test case 2: Key not in the white list, but not matching any black list regex
result = logger.nameFilter("notblacklisted")
if result != true {
t.Errorf("Expected: true, Got: %v", result)
}

// Test case 3: Key in the black list
result = logger.nameFilter("blacklisted-key")
if result != false {
t.Errorf("Expected: false, Got: %v", result)
}

// Test case 4: Key not in the white list and matches a black list regex
result = logger.nameFilter("blacklisted")
if result != false {
t.Errorf("Expected: false, Got: %v", result)
}

// Test case 5: No white or black list
logger = &Logger{} // Reset the logger
result = logger.nameFilter("anykey")
if result != true {
t.Errorf("Expected: true, Got: %v", result)
}
}
39 changes: 19 additions & 20 deletions adapters/devices/sofar/lsw.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,18 @@ 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 {
reply, err := readRegisterRange(rr, connPort, serialNumber)
if err != nil {
return nil, err
}

for k, v := range reply {
result[k] = v
if nameFilter(k) {
result[k] = v
}
}
}
return result, nil
Expand Down Expand Up @@ -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

Expand Down
36 changes: 26 additions & 10 deletions adapters/devices/sofar/sofar_protocol.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package sofar

import (
"github.com/kubaceg/sofar_g3_lsw3_logger_reader/ports"
)

type field struct {
register int
name string
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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", "", ""},
Expand All @@ -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"},
Expand Down
Loading