Skip to content

Commit bbdd5c8

Browse files
authored
Merge pull request #1 from lazy-electron-consulting/prototype
Prototype
2 parents 76d81a2 + c72b8de commit bbdd5c8

File tree

14 files changed

+916
-5
lines changed

14 files changed

+916
-5
lines changed

.vscode/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,12 @@
33
"Victron"
44
],
55
"go.lintTool": "golangci-lint",
6-
"go.testFlags": ["-race"],
6+
"go.testFlags": ["-race", "-count=1"],
7+
"go.addTags": {
8+
"tags": "json,yaml",
9+
"options": "json=omitempty,yaml=omitempty",
10+
"promptForTags": false,
11+
"transform": "camelcase",
12+
"template": ""
13+
}
714
}

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
11
# ve-direct-exporter
2-
prometheus exporter for victron devices using the ve-direct protocol
2+
3+
[Prometheus exporter](https://prometheus.io/docs/instrumenting/exporters/) for [Victron](https://www.victronenergy.com) devices using the [VE.Direct protocol](https://www.victronenergy.com/live/vedirect_protocol:faq).
4+
5+
## Usage
6+
7+
- download a binary from the github releases or build from source using `make
8+
build`
9+
- connect your computer to a victron device, I'm using a victron brand [VE.Direct to USB interface](https://www.victronenergy.com/accessories/ve-direct-to-usb-interface)
10+
- run `./vedirect-exporter config.yaml`
11+
- see your metrics via http
12+
13+
See `configs/` for an example of the config syntax.
14+
15+
## Releasing
16+
17+
- merging to `main` will make new "latest" binaries
18+
- push a tag w/ `v$SEMVER` to make a versioned binaries

cmd/vedirect_exporter/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os/signal"
1111
"syscall"
1212

13+
"github.com/lazy-electron-consulting/ve-direct-exporter/internal/config"
1314
"github.com/lazy-electron-consulting/ve-direct-exporter/internal/start"
1415
)
1516

@@ -25,9 +26,14 @@ func main() {
2526
os.Exit(1)
2627
}
2728

29+
cfg, err := config.ReadYaml(flag.Arg(0))
30+
if err != nil {
31+
log.Fatalf("could not read config %s: %v", flag.Arg(0), err)
32+
}
33+
2834
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGHUP, syscall.SIGABRT)
2935
defer stop()
30-
err := start.Run(ctx)
36+
err = start.Run(ctx, cfg)
3137
if err != nil && !errors.Is(err, context.Canceled) {
3238
log.Fatalf("exiting with errors %v\n", err)
3339
}

configs/smartshunt500.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
serial:
2+
path: /dev/ttyUSB0
3+
baudRate: 19200
4+
dataBits: 8
5+
stopBits: 1
6+
parity: N
7+
subsystem: smart_shunt
8+
gauges:
9+
- name: battery_volts
10+
help: Main battery voltage
11+
label: V
12+
multiplier: 0.001
13+
- name: battery_amps
14+
help: Main battery current
15+
label: I
16+
multiplier: 0.001
17+
- name: time_remaining_minutes
18+
help: How long until the battery is empty
19+
label: TTG

go.mod

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
11
module github.com/lazy-electron-consulting/ve-direct-exporter
22

33
go 1.18
4+
5+
require (
6+
github.com/goburrow/serial v0.1.0
7+
github.com/stretchr/testify v1.7.1
8+
)
9+
10+
require (
11+
github.com/beorn7/perks v1.0.1 // indirect
12+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
13+
github.com/golang/protobuf v1.5.2 // indirect
14+
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
15+
github.com/prometheus/client_model v0.2.0 // indirect
16+
github.com/prometheus/common v0.32.1 // indirect
17+
github.com/prometheus/procfs v0.7.3 // indirect
18+
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
19+
google.golang.org/protobuf v1.26.0 // indirect
20+
)
21+
22+
require (
23+
github.com/davecgh/go-spew v1.1.1 // indirect
24+
github.com/pmezard/go-difflib v1.0.0 // indirect
25+
github.com/prometheus/client_golang v1.12.1
26+
gopkg.in/yaml.v2 v2.4.0
27+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
28+
)

go.sum

Lines changed: 475 additions & 0 deletions
Large diffs are not rendered by default.

internal/config/config.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"time"
8+
9+
"github.com/lazy-electron-consulting/ve-direct-exporter/internal/util"
10+
"gopkg.in/yaml.v2"
11+
)
12+
13+
const (
14+
DefaultAddress = ":8000"
15+
DefaultBaudRate = 19200
16+
DefaultDataBits = 8
17+
DefaultStopBits = 1
18+
DefaultParity = "N"
19+
DefaultTimeout = 5 * time.Second
20+
)
21+
22+
type Serial struct {
23+
Path string `json:"path,omitempty" yaml:"path,omitempty"`
24+
BaudRate int `json:"baudRate,omitempty" yaml:"baudRate,omitempty"`
25+
DataBits int `json:"dataBits,omitempty" yaml:"dataBits,omitempty"`
26+
StopBits int `json:"stopBits,omitempty" yaml:"stopBits,omitempty"`
27+
Parity string `json:"parity,omitempty" yaml:"parity,omitempty"`
28+
Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
29+
}
30+
31+
func (s *Serial) defaults() {
32+
s.BaudRate = util.Default(s.BaudRate, DefaultBaudRate)
33+
s.DataBits = util.Default(s.DataBits, DefaultDataBits)
34+
s.StopBits = util.Default(s.StopBits, DefaultStopBits)
35+
s.Parity = util.Default(s.Parity, DefaultParity)
36+
s.Timeout = util.Default(s.Timeout, DefaultTimeout)
37+
}
38+
39+
type Gauge struct {
40+
Name string `json:"name,omitempty" yaml:"name,omitempty"`
41+
Label string `json:"label,omitempty" yaml:"label,omitempty"`
42+
Help string `json:"help,omitempty" yaml:"help,omitempty"`
43+
Multiplier float64 `json:"multiplier,omitempty" yaml:"multiplier,omitempty"`
44+
}
45+
46+
func (g *Gauge) defaults() {
47+
g.Multiplier = util.Default(g.Multiplier, 1)
48+
}
49+
50+
type Config struct {
51+
Address string `json:"address,omitempty" yaml:"address,omitempty"`
52+
Subsystem string `json:"subsystem,omitempty" yaml:"subsystem,omitempty"`
53+
Serial Serial `json:"serial,omitempty" yaml:"serial,omitempty"`
54+
Gauges []Gauge `json:"gauges,omitempty" yaml:"gauges,omitempty"`
55+
}
56+
57+
func (c *Config) defaults() {
58+
c.Address = util.Default(c.Address, DefaultAddress)
59+
c.Serial.defaults()
60+
for i, g := range c.Gauges {
61+
g.defaults()
62+
c.Gauges[i] = g
63+
}
64+
}
65+
66+
func ParseYaml(r io.Reader) (*Config, error) {
67+
decoder := yaml.NewDecoder(r)
68+
decoder.SetStrict(true)
69+
70+
var config Config
71+
if err := decoder.Decode(&config); err != nil {
72+
return nil, fmt.Errorf("failed to parse: %w", err)
73+
}
74+
config.defaults()
75+
return &config, nil
76+
}
77+
78+
func ReadYaml(path string) (*Config, error) {
79+
f, err := os.Open(path)
80+
if err != nil {
81+
return nil, err
82+
}
83+
return ParseYaml(f)
84+
}

internal/config/config_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package config_test
2+
3+
import (
4+
"embed"
5+
"testing"
6+
"time"
7+
8+
"github.com/lazy-electron-consulting/ve-direct-exporter/internal/config"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
//go:embed testdata
13+
var testdata embed.FS
14+
15+
func TestParseYaml(t *testing.T) {
16+
t.Parallel()
17+
18+
tests := map[string]struct {
19+
path string
20+
want *config.Config
21+
}{
22+
"full": {
23+
path: "testdata/full.yml",
24+
want: &config.Config{
25+
Address: ":9090",
26+
Serial: config.Serial{
27+
Path: "/dev/ttyUSB0",
28+
BaudRate: 9600,
29+
DataBits: 7,
30+
StopBits: 2,
31+
Parity: "Y",
32+
Timeout: time.Hour,
33+
},
34+
Subsystem: "test",
35+
Gauges: []config.Gauge{
36+
{
37+
Name: "battery_volts",
38+
Help: "Main battery voltage",
39+
Label: "V",
40+
Multiplier: 0.01,
41+
},
42+
},
43+
},
44+
},
45+
"empty": {
46+
path: "testdata/empty.yml",
47+
want: &config.Config{
48+
Address: config.DefaultAddress,
49+
Serial: config.Serial{
50+
Path: "/dev/ttyUSB1",
51+
BaudRate: config.DefaultBaudRate,
52+
DataBits: config.DefaultDataBits,
53+
StopBits: config.DefaultStopBits,
54+
Parity: config.DefaultParity,
55+
Timeout: config.DefaultTimeout,
56+
},
57+
Gauges: []config.Gauge{
58+
{
59+
Name: "foo",
60+
Multiplier: 1,
61+
},
62+
},
63+
},
64+
},
65+
}
66+
for name, tc := range tests {
67+
tc := tc
68+
t.Run(name, func(t *testing.T) {
69+
t.Parallel()
70+
71+
file, err := testdata.Open(tc.path)
72+
require.NoError(t, err)
73+
74+
got, err := config.ParseYaml(file)
75+
require.NoError(t, err)
76+
require.EqualValues(t, tc.want, got)
77+
})
78+
}
79+
}

internal/config/testdata/empty.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
serial:
2+
path: /dev/ttyUSB1
3+
gauges:
4+
- name: foo

internal/config/testdata/full.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
address: :9090
2+
serial:
3+
path: /dev/ttyUSB0
4+
baudRate: 9600
5+
dataBits: 7
6+
stopBits: 2
7+
parity: Y
8+
timeout: 60m
9+
subsystem: test
10+
gauges:
11+
- name: battery_volts
12+
help: Main battery voltage
13+
label: V
14+
multiplier: 0.01

0 commit comments

Comments
 (0)