46
Makefile
Normal file
46
Makefile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
PROJECT_NAME := pvcollect
|
||||||
|
|
||||||
|
PREFIX ?= /usr/bin
|
||||||
|
|
||||||
|
CONFIG_DIR := /etc/$(PROJECT_NAME)
|
||||||
|
SYSTEM_DIR := /usr/lib/systemd/system
|
||||||
|
|
||||||
|
CONFIG_FILE := config/config.json
|
||||||
|
BIN_FILE := bin/$(PROJECT_NAME)
|
||||||
|
UNIT_FILE := pvcollect.service
|
||||||
|
README_FILE := README.md
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all:
|
||||||
|
mkdir -p bin
|
||||||
|
go build -buildvcs=false -o $(BIN_FILE)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm -rf bin
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: all
|
||||||
|
@if [ -f $(CONFIG_DIR)/$(notdir $(CONFIG_FILE)) ]; then \
|
||||||
|
echo "$(CONFIG_DIR)/$(notdir $(CONFIG_FILE)) already exists - skipping..."; \
|
||||||
|
else \
|
||||||
|
install -d $(CONFIG_DIR); \
|
||||||
|
install -m 0644 $(CONFIG_FILE) $(CONFIG_DIR); \
|
||||||
|
echo "install -d $(CONFIG_DIR)"; \
|
||||||
|
echo "install -m 0644 $(CONFIG_FILE) $(CONFIG_DIR)"; \
|
||||||
|
fi
|
||||||
|
install -d $(PREFIX)
|
||||||
|
install -m 0755 $(BIN_FILE) $(PREFIX)
|
||||||
|
install -d $(SYSTEM_DIR)
|
||||||
|
install -m 0644 $(UNIT_FILE) $(SYSTEM_DIR)
|
||||||
|
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall:
|
||||||
|
rm -rf $(CONFIG_DIR)
|
||||||
|
rm -rf $(SYSTEM_DIR)/$(UNIT_FILE)
|
||||||
|
rm -rf $(PREFIX)/$(PROJECT_NAME)
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package: all
|
||||||
|
tar cvzf $(PROJECT_NAME).tar.gz $(CONFIG_FILE) $(BIN_FILE) $(UNIT_FILE) $(README_FILE)
|
BIN
bin/pvcollect
Executable file
BIN
bin/pvcollect
Executable file
Binary file not shown.
92
config/config.json
Normal file
92
config/config.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"sample_rate": 10,
|
||||||
|
"influxdb_host": "p5.local",
|
||||||
|
"influxdb_port": 8086,
|
||||||
|
"influxdb_token": "",
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"name": "AlphaEss",
|
||||||
|
"type": "pv",
|
||||||
|
"modbus": {
|
||||||
|
"protocol": "tcp",
|
||||||
|
"host": "192.168.178.79",
|
||||||
|
"port": 502,
|
||||||
|
"slave_address": 85
|
||||||
|
},
|
||||||
|
"registers": [
|
||||||
|
{
|
||||||
|
"name": "TotalEnergyFeedToGridGrid",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 16,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 0.01,
|
||||||
|
"unit": "kWh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TotalEnergyConsumeFromGridGrid",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 18,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 0.01,
|
||||||
|
"unit": "kWh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pv1Power",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 1055,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 1,
|
||||||
|
"unit": "W"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pv2Power",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 1059,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 1,
|
||||||
|
"unit": "W"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InverterTotalPvEnergy",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 1086,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 0.1,
|
||||||
|
"unit": "kWh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InverterPowerL1",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 1030,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 1,
|
||||||
|
"unit": "W"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InverterPowerL2",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 1032,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 1,
|
||||||
|
"unit": "W"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InverterPowerL3",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 1034,
|
||||||
|
"quantity": 2,
|
||||||
|
"factor": 1,
|
||||||
|
"unit": "W"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BatteryStateOfCharge",
|
||||||
|
"type": "holding",
|
||||||
|
"address": 258,
|
||||||
|
"quantity": 1,
|
||||||
|
"factor": 0.1,
|
||||||
|
"unit": "%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
17
go.mod
Normal file
17
go.mod
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module pvcollect
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||||
|
github.com/simonvetter/modbus v1.6.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
|
github.com/goburrow/serial v0.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.3.1 // indirect
|
||||||
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||||
|
github.com/oapi-codegen/runtime v1.0.0 // indirect
|
||||||
|
golang.org/x/net v0.23.0 // indirect
|
||||||
|
)
|
31
go.sum
Normal file
31
go.sum
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||||
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
|
||||||
|
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
|
||||||
|
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||||
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
|
||||||
|
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
|
||||||
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
|
||||||
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||||
|
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||||
|
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
|
||||||
|
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
|
||||||
|
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/simonvetter/modbus v1.6.3 h1:kDzwVfIPczsM4Iz09il/Dij/bqlT4XiJVa0GYaOVA9w=
|
||||||
|
github.com/simonvetter/modbus v1.6.3/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd8q+GVRObPw=
|
||||||
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||||
|
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
165
main.go
Normal file
165
main.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/simonvetter/modbus"
|
||||||
|
|
||||||
|
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mod_bus struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
BaudRate int `json:"baud_rate"`
|
||||||
|
SlaveAddress int `json:"slave_address"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type register struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Address int `json:"address"`
|
||||||
|
Quantity int `json:"quantity"`
|
||||||
|
Factor float32 `json:"factor"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type device struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Modbus mod_bus `json:"modbus"`
|
||||||
|
Registers []register `json:"registers"`
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
SampleRate int `json:"sample_rate"`
|
||||||
|
InfluxdbHost string `json:"influxdb_host"`
|
||||||
|
InfluxdbPort int `json:"influxdb_port"`
|
||||||
|
InfluxdbToken string `json:"influxdb_token"`
|
||||||
|
Devices []device `json:"devices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger log.Logger = *log.Default()
|
||||||
|
config_path string
|
||||||
|
config_cache config
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.SetFlags(log.Llongfile | log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func read_config() {
|
||||||
|
data, err := os.ReadFile(config_path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Unable to read %s", config_path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &config_cache)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print("Unable to evaluate config data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&config_path, "c", "../config/config.json", "Specify path to find the config file. Default is ../config/config.json")
|
||||||
|
flag.Parse()
|
||||||
|
read_config()
|
||||||
|
|
||||||
|
type remote struct {
|
||||||
|
client *modbus.ModbusClient
|
||||||
|
registers []register
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var remotes []remote
|
||||||
|
var client *modbus.ModbusClient
|
||||||
|
|
||||||
|
// setup influx connection
|
||||||
|
influxdb_url := "http://" + config_cache.InfluxdbHost + ":" + strconv.Itoa(config_cache.InfluxdbPort)
|
||||||
|
db_client := influxdb2.NewClient(influxdb_url, config_cache.InfluxdbToken)
|
||||||
|
defer db_client.Close()
|
||||||
|
ctx := context.Background()
|
||||||
|
write_api := db_client.WriteAPIBlocking("tkl", "home")
|
||||||
|
|
||||||
|
for _, v := range config_cache.Devices {
|
||||||
|
if v.Modbus.Protocol == "tcp" {
|
||||||
|
url := v.Modbus.Protocol + "://" + v.Modbus.Host + ":" + strconv.Itoa(v.Modbus.Port)
|
||||||
|
client, err = modbus.NewClient(&modbus.ClientConfiguration{URL: url})
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed to create modbus client: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client.SetUnitId(0x55)
|
||||||
|
err = client.Open()
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed to connect: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp remote
|
||||||
|
tmp.client = client
|
||||||
|
tmp.registers = v.Registers
|
||||||
|
tmp.name = v.Name
|
||||||
|
remotes = append(remotes, tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for { // ever
|
||||||
|
for _, remote := range remotes {
|
||||||
|
point := influxdb2.NewPointWithMeasurement(remote.name)
|
||||||
|
for _, reg := range remote.registers {
|
||||||
|
res, err := remote.client.ReadRegisters(uint16(reg.Address), uint16(reg.Quantity), modbus.HOLDING_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed to read: %v\n", err)
|
||||||
|
error_strings := []string{
|
||||||
|
"connection reset by peer",
|
||||||
|
"broken pipe",
|
||||||
|
}
|
||||||
|
for _, error_string := range error_strings {
|
||||||
|
if strings.Contains(err.Error(), error_string) {
|
||||||
|
logger.Print("trying to restart modbus client")
|
||||||
|
remote.client.Close()
|
||||||
|
err = remote.client.Open()
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("failed to restart: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var tmp int32
|
||||||
|
switch reg.Quantity {
|
||||||
|
case 1:
|
||||||
|
tmp = int32(res[0])
|
||||||
|
case 2:
|
||||||
|
tmp = ((int32(res[0]) << 16) | int32(res[1]))
|
||||||
|
default:
|
||||||
|
logger.Printf("Unsupported quantity of registers (%d)", reg.Quantity)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := float32(tmp) * reg.Factor
|
||||||
|
point.AddField(reg.Name, result)
|
||||||
|
}
|
||||||
|
point.SetTime(time.Now())
|
||||||
|
err := write_api.WritePoint(ctx, point)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
}
|
||||||
|
err = write_api.Flush(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Print(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(config_cache.SampleRate * 1000000000))
|
||||||
|
}
|
||||||
|
}
|
12
pvcollect.service
Normal file
12
pvcollect.service
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=pv collect service
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
ExecStart=/usr/bin/pvcollect -c /etc/pvcollect/config.json
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
Reference in New Issue
Block a user