Skip to content

Commit c72b8de

Browse files
committed
initial prototype
Very similar to `renogy-exporter`, but slightly different syntax. Might be possible to merge the two later with a sufficiently complicated config file.
1 parent 688b117 commit c72b8de

File tree

12 files changed

+711
-23
lines changed

12 files changed

+711
-23
lines changed

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: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ serial:
44
dataBits: 8
55
stopBits: 1
66
parity: N
7+
subsystem: smart_shunt
78
gauges:
8-
- name: "battery_volts"
9-
help: "Main battery voltage"
10-
# matches the thing from ve-direct
11-
label: "V"
12-
multiplier: 0.01
13-
# TODO: OTHERS
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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,21 @@ require (
88
)
99

1010
require (
11-
github.com/davecgh/go-spew v1.1.0 // indirect
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
1224
github.com/pmezard/go-difflib v1.0.0 // indirect
25+
github.com/prometheus/client_golang v1.12.1
1326
gopkg.in/yaml.v2 v2.4.0
1427
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
1528
)

go.sum

Lines changed: 462 additions & 2 deletions
Large diffs are not rendered by default.

internal/config/config.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,27 @@ type Gauge struct {
4040
Name string `json:"name,omitempty" yaml:"name,omitempty"`
4141
Label string `json:"label,omitempty" yaml:"label,omitempty"`
4242
Help string `json:"help,omitempty" yaml:"help,omitempty"`
43-
Multiplier float32 `json:"multiplier,omitempty" yaml:"multiplier,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)
4448
}
4549

4650
type Config struct {
47-
Address string `json:"address,omitempty" yaml:"address,omitempty"`
48-
Serial Serial `json:"serial,omitempty" yaml:"serial,omitempty"`
49-
Gauges []Gauge `json:"gauges,omitempty" yaml:"gauges,omitempty"`
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"`
5055
}
5156

5257
func (c *Config) defaults() {
5358
c.Address = util.Default(c.Address, DefaultAddress)
5459
c.Serial.defaults()
55-
60+
for i, g := range c.Gauges {
61+
g.defaults()
62+
c.Gauges[i] = g
63+
}
5664
}
5765

5866
func ParseYaml(r io.Reader) (*Config, error) {

internal/config/config_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func TestParseYaml(t *testing.T) {
3131
Parity: "Y",
3232
Timeout: time.Hour,
3333
},
34+
Subsystem: "test",
3435
Gauges: []config.Gauge{
3536
{
3637
Name: "battery_volts",
@@ -53,6 +54,12 @@ func TestParseYaml(t *testing.T) {
5354
Parity: config.DefaultParity,
5455
Timeout: config.DefaultTimeout,
5556
},
57+
Gauges: []config.Gauge{
58+
{
59+
Name: "foo",
60+
Multiplier: 1,
61+
},
62+
},
5663
},
5764
},
5865
}

internal/config/testdata/empty.yml

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

internal/config/testdata/full.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ serial:
66
stopBits: 2
77
parity: Y
88
timeout: 60m
9+
subsystem: test
910
gauges:
10-
- name: "battery_volts"
11-
help: "Main battery voltage"
12-
# matches the thing from ve-direct
13-
label: "V"
11+
- name: battery_volts
12+
help: Main battery voltage
13+
label: V
1414
multiplier: 0.01

internal/metrics/metrics.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package metrics
2+
3+
import (
4+
"context"
5+
"log"
6+
"strconv"
7+
8+
"github.com/lazy-electron-consulting/ve-direct-exporter/internal/config"
9+
"github.com/lazy-electron-consulting/ve-direct-exporter/internal/scanner"
10+
"github.com/prometheus/client_golang/prometheus"
11+
"github.com/prometheus/client_golang/prometheus/promauto"
12+
)
13+
14+
type gauge struct {
15+
prometheus.Gauge
16+
cfg config.Gauge
17+
}
18+
19+
func newGauge(subsystem string, cfg config.Gauge) gauge {
20+
return gauge{
21+
Gauge: promauto.NewGauge(prometheus.GaugeOpts{
22+
Subsystem: subsystem,
23+
Name: cfg.Name,
24+
Help: cfg.Help,
25+
}),
26+
cfg: cfg,
27+
}
28+
}
29+
30+
func (g gauge) update(value string) error {
31+
i, err := strconv.Atoi(value)
32+
if err != nil {
33+
return err
34+
}
35+
v := float64(i) * g.cfg.Multiplier
36+
g.Set(v)
37+
return nil
38+
}
39+
40+
type Registry struct {
41+
gauges map[string]gauge
42+
}
43+
44+
func New(subsystem string, gs []config.Gauge) (*Registry, error) {
45+
r := Registry{
46+
gauges: make(map[string]gauge, len(gs)),
47+
}
48+
49+
for _, g := range gs {
50+
r.gauges[g.Label] = newGauge(subsystem, g)
51+
}
52+
53+
return &r, nil
54+
}
55+
56+
func (r *Registry) Run(ctx context.Context, in <-chan scanner.Reading) {
57+
for {
58+
select {
59+
case <-ctx.Done():
60+
return
61+
case d, ok := <-in:
62+
if !ok {
63+
return
64+
}
65+
err := r.update(d.Label, d.Value)
66+
if err != nil {
67+
log.Printf("failed to update %v, %v\n", d, err)
68+
}
69+
}
70+
}
71+
}
72+
73+
func (r *Registry) update(label, value string) error {
74+
g, ok := r.gauges[label]
75+
if ok {
76+
return g.update(value)
77+
}
78+
return nil
79+
}

0 commit comments

Comments
 (0)