diff --git a/.dockerignore b/.dockerignore index adfefb3c..428a6059 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ .vscode dist/ +vendor/ diff --git a/.gitignore b/.gitignore index 4b07d48c..1e62693d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode/ +.idea/ *.json *.sh *.yaml @@ -7,3 +8,4 @@ linux-*.Dockerfile mbmd !entrypoint.sh !mbmd.dist.yaml +vendor/ diff --git a/Dockerfile b/Dockerfile index be96ea79..3b377a48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,26 +3,25 @@ ############################ FROM golang:alpine as builder +RUN --mount=type=tmpfs,target=/build +WORKDIR /build + # Install git + SSL ca certificates. # Git is required for fetching the dependencies. # Ca-certificates is required to call HTTPS endpoints. RUN apk update && apk add --no-cache git ca-certificates tzdata alpine-sdk && update-ca-certificates -WORKDIR /build - -# cache modules COPY go.mod . COPY go.sum . RUN go mod download COPY . . -RUN make install RUN make build ############################# ## STEP 2 build a small image ############################# -FROM alpine +FROM alpine:latest # Import from builder. COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo diff --git a/README.md b/README.md index bb4a6dce..1f232626 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,15 @@ By default, readings are published at `/mbmd//`. Rate limiti ## InfluxDB support -There is also the option to directly insert the data into an influxdb database by using the command-line options available. InfluxDB 1.8 and 2.0 are currently supported. to enable this, add the `--influx-database` and the `--influx-url` commandline parameter. More advanced configuration is available, to learn more checkout the [mbmd_run.md](docs/mbmd_run.md) documentation +There is also the option to directly insert the data into an influxdb database +by using the command-line options available. InfluxDB 1.8 and 2.0 are currently +supported. to enable this, add the `--influx-database` and the `--influx-url` +commandline parameter. More advanced configuration is available, to learn more +checkout the [mbmd_run.md](docs/mbmd_run.md) documentation + +## Prometheus support + +There is also the option to pull prometheus data via the `/metrics` endpoint. # Supported Devices @@ -297,7 +305,7 @@ Apart from meters, SunSpec-compatible grid inverters connected over TCP are supported, too. SunSpec defines a default register layout for accessing the devices. -Supported inverters include popular devices from SolarEdge (SE3000, SE9000) +Supported inverters include popular devices from SolarEdge (SE3000, SE9000, SE17K, SE10K-RWS) and SMA (Sunny Boy and Sunny TriPower). In case of TCP connection, the adapter parameter becomes the hostname and port: diff --git a/assets/index.html b/assets/index.html index 4d8c7bff..f14b5aa6 100644 --- a/assets/index.html +++ b/assets/index.html @@ -390,6 +390,8 @@

Status

Meter Type + Model + Serial Status @@ -397,6 +399,8 @@

Status

${ idx } ${ m.Type } + ${ m.Model } + ${ m.Serial } ${ m.Status } @@ -406,7 +410,8 @@

Status

About MBMD

MBMD collects measurements from modbus devices. - It works with meters like the Eastron SDM630 as well as grid inverters like SMA Sunny Boy.
+ It works with meters like the Eastron SDM630 as well as grid inverters like SMA Sunny Boy + and all other inverters implementing the SUNSPEC standard.
Please refer to the documentation for more information.

This installation runs MBMD version {{.SoftwareVersion}} (compiled with {{.GolangVersion}})

diff --git a/assets/js/app.js b/assets/js/app.js index 2ec8e4bc..287d8e9c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -130,9 +130,13 @@ function processMessage(data) { function connectSocket() { var ws, loc = window.location; - var protocol = loc.protocol == "https:" ? "wss:" : "ws:" + var protocol = loc.protocol === "https:" ? "wss:" : "ws:" + var path = loc.pathname.replace(/\/$/, "") + if (path === "") { + path = "/" + } - ws = new WebSocket(protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname.replace(/\/$/, "") + "/ws"); + ws = new WebSocket(protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + path + "ws"); ws.onerror = function(evt) { ws.close(); diff --git a/cmd/confighandler.go b/cmd/confighandler.go index 284debb9..724dad7c 100644 --- a/cmd/confighandler.go +++ b/cmd/confighandler.go @@ -3,7 +3,6 @@ package cmd import ( "os" "regexp" - "sort" "strconv" "strings" "time" @@ -15,13 +14,20 @@ import ( // Config describes the entire configuration type Config struct { - API string - Rate time.Duration - Mqtt MqttConfig - Influx InfluxConfig - Adapters []AdapterConfig - Devices []DeviceConfig - Other map[string]interface{} `mapstructure:",remain"` + API string + Rate time.Duration + Mqtt MqttConfig + Influx InfluxConfig + Adapters []AdapterConfig + Devices []DeviceConfig + Prometheus PrometheusConfig + Other map[string]interface{} `mapstructure:",remain"` +} + +type PrometheusConfig struct { + Enable bool // defaults to yes + EnableProcessCollector bool + EnableGoCollector bool } // MqttConfig describes the mqtt broker configuration @@ -117,35 +123,38 @@ func (conf *DeviceConfigHandler) ConnectionManager(connSpec string, rtu bool, ba return manager } +var sunspecTypes = map[string]bool{ + "FRONIUS": true, + "KACO": true, + "KOSTAL": true, + "SE": true, + "SMA": true, + "SOLAREDGE": true, + "STECA": true, + "SUNS": true, + "SUNSPEC": true, +} + func (conf *DeviceConfigHandler) createDeviceForManager( manager *meters.Manager, + name string, meterType string, subdevice int, ) meters.Device { var meter meters.Device meterType = strings.ToUpper(meterType) - var isSunspec bool - sunspecTypes := []string{"FRONIUS", "KOSTAL", "KACO", "SE", "SMA", "SOLAREDGE", "STECA", "SUNS", "SUNSPEC"} - for _, t := range sunspecTypes { - if t == meterType { - isSunspec = true - break - } - } - - sort.SearchStrings(sunspecTypes, meterType) - if isSunspec { - meter = sunspec.NewDevice(meterType, subdevice) + if sunspecTypes[meterType] { + meter = sunspec.NewDevice(name, meterType, subdevice) } else { if subdevice > 0 { - log.Fatalf("Invalid subdevice number for device %s: %d", meterType, subdevice) + log.Fatalf("Invalid subdevice number for device '%s' (%s): %d", name, meterType, subdevice) } var err error - meter, err = rs485.NewDevice(meterType) + meter, err = rs485.NewDevice(name, meterType) if err != nil { - log.Fatalf("Error creating device %s: %v.", meterType, err) + log.Fatalf("Error creating device '%s' (%s): %v.", name, meterType, err) } } @@ -170,7 +179,7 @@ func (conf *DeviceConfigHandler) CreateDevice(devConf DeviceConfig) { if !ok { log.Fatalf("Missing adapter configuration for device %v", devConf) } - meter := conf.createDeviceForManager(manager, devConf.Type, devConf.SubDevice) + meter := conf.createDeviceForManager(manager, devConf.Name, devConf.Type, devConf.SubDevice) if err := manager.Add(devConf.ID, meter); err != nil { log.Fatalf("Error adding device %v: %v.", devConf, err) @@ -226,7 +235,7 @@ func (conf *DeviceConfigHandler) CreateDeviceFromSpec(deviceDef string, timeout // have been created of the --rtu flag was specified. We'll not re-check this here. manager := conf.ConnectionManager(connSpec, false, 0, "", timeout) - meter := conf.createDeviceForManager(manager, meterType, subdevice) + meter := conf.createDeviceForManager(manager, "", meterType, subdevice) if err := manager.Add(uint8(id), meter); err != nil { log.Fatalf("Error adding device %s: %v. See -h for help.", meterDef, err) } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 00000000..1beea0cf --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,4 @@ +package cmd + +// Yeah, it compiles +var _ = Execute diff --git a/cmd/run.go b/cmd/run.go index 9acceb94..d99a902c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/pflag" "github.com/spf13/viper" latest "github.com/tcnksm/go-latest" - + "github.com/volkszaehler/mbmd/prometheus" "github.com/volkszaehler/mbmd/server" ) @@ -223,6 +223,7 @@ func run(cmd *cobra.Command, args []string) { } } + promCfg := PrometheusConfig{Enable: true} if cfgFile != "" { // config file found log.Printf("config: using %s", viper.ConfigFileUsed()) @@ -247,7 +248,14 @@ func run(cmd *cobra.Command, args []string) { confHandler.CreateDevice(dev) } } + promCfg = conf.Prometheus } + // Prometheus manager - Register all static metrics to default registry + prometheus.RegisterAllMetrics(prometheus.Config{ + Enable: promCfg.Enable, + EnableProcessCollector: promCfg.EnableProcessCollector, + EnableGoCollector: promCfg.EnableGoCollector, + }) if countDevices(confHandler.Managers) == 0 { log.Fatal("config: no devices found - terminating") diff --git a/cmd/scan.go b/cmd/scan.go index bf3d72fd..30e288ff 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -79,11 +79,11 @@ func scan(cmd *cobra.Command, args []string) { // create devices devices := make([]meters.Device, 0) if _, ok := conn.(*meters.TCP); ok { - suns := sunspec.NewDevice("SUNS") + suns := sunspec.NewDevice("", "SUNS") devices = append(devices, suns) } else { for t := range rs485.Producers { - dev, err := rs485.NewDevice(t) + dev, err := rs485.NewDevice("", t) if err != nil { log.Fatal(err) } @@ -109,14 +109,16 @@ SCAN: if !errors.Is(err, meters.ErrPartiallyOpened) { continue // devices } + log.Println(err) // log error but continue } mr, err := dev.Probe(client) if err == nil && v.check(mr.Value) { + deviceDescriptor := dev.Descriptor() log.Printf("device %d: %s type device found, %s: %.2f\r\n", deviceID, - dev.Descriptor().Manufacturer, + deviceDescriptor.Manufacturer, mr.Measurement, mr.Value, ) diff --git a/go.mod b/go.mod index 885f7f7d..a27081e3 100644 --- a/go.mod +++ b/go.mod @@ -1,62 +1,67 @@ module github.com/volkszaehler/mbmd -go 1.21 - -toolchain go1.21.5 +go 1.23 require ( - github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912 - github.com/dmarkham/enumer v1.5.9 - github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b + github.com/dmarkham/enumer v1.5.10 + github.com/eclipse/paho.mqtt.golang v1.5.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 - github.com/gorilla/websocket v1.5.1 - github.com/grid-x/modbus v0.0.0-20240429072715-02314cc902aa - github.com/influxdata/influxdb-client-go/v2 v2.13.0 - github.com/spf13/cobra v1.8.0 + github.com/gorilla/websocket v1.5.3 + github.com/grid-x/modbus v0.0.0-20241004123532-f6c6fb5201b3 + github.com/influxdata/influxdb-client-go/v2 v2.14.0 + github.com/prometheus/client_golang v1.20.5 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.18.2 + github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/pascaldekloe/name v1.0.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.20.0 // indirect - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.27.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9c75f731..bfd90aeb 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,24 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912 h1:pphS+WUa9zK0ZH/Rt2JMEaOx0qz8dDzOzbr/W7BMiXQ= -github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k= +github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b h1:81UMfM949I7StrRay7YDUZazY8M1u/JHkzwcFEjiilQ= +github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k= 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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dmarkham/enumer v1.5.9 h1:NM/1ma/AUNieHZg74w67GkHFBNB15muOt3sj486QVZk= -github.com/dmarkham/enumer v1.5.9/go.mod h1:e4VILe2b1nYK3JKJpRmNdl5xbDQvELc6tQ8b+GsGk6E= -github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= -github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/dmarkham/enumer v1.5.10 h1:ygL0L6quiTiH1jpp68DyvsWaea6MaZLZrTTkIS++R0M= +github.com/dmarkham/enumer v1.5.10/go.mod h1:e4VILe2b1nYK3JKJpRmNdl5xbDQvELc6tQ8b+GsGk6E= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/evcc-io/modbus v0.0.0-20240915144537-980a0405c373 h1:+LFx0Rik2XlqQM8sq3VW3c8DbxN2hKBkjjusniBSsAs= github.com/evcc-io/modbus v0.0.0-20240915144537-980a0405c373/go.mod h1:WpbUAyptAAi0VAriSRopZa6uhiJOJCTz7KFvgGtNRXc= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= @@ -22,8 +26,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -37,71 +41,75 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa h1:Rsn6ARgNkXrsXJIzhkE4vQr5Gbx2LvtEMv4BJOK4LyU= github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= -github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= +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-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0= github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -110,27 +118,28 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= diff --git a/meters/device.go b/meters/device.go index 8fe614e2..424c67e3 100644 --- a/meters/device.go +++ b/meters/device.go @@ -6,6 +6,7 @@ import ( // DeviceDescriptor describes a device type DeviceDescriptor struct { + Name string Type string Manufacturer string Model string diff --git a/meters/measurement_enumer.go b/meters/measurement_enumer.go index f7d03e89..08aefef3 100644 --- a/meters/measurement_enumer.go +++ b/meters/measurement_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _MeasurementName = "FrequencyCurrentCurrentL1CurrentL2CurrentL3VoltageVoltageL1VoltageL2VoltageL3PowerPowerL1PowerL2PowerL3ImportPowerImportPowerL1ImportPowerL2ImportPowerL3ExportPowerExportPowerL1ExportPowerL2ExportPowerL3ReactivePowerReactivePowerL1ReactivePowerL2ReactivePowerL3ApparentPowerApparentPowerL1ApparentPowerL2ApparentPowerL3CosphiCosphiL1CosphiL2CosphiL3THDTHDL1THDL2THDL3SumSumT1SumT2SumL1SumL2SumL3ImportImportT1ImportT2ImportL1ImportL2ImportL3ExportExportT1ExportT2ExportL1ExportL2ExportL3ReactiveSumReactiveSumT1ReactiveSumT2ReactiveSumL1ReactiveSumL2ReactiveSumL3ReactiveImportReactiveImportT1ReactiveImportT2ReactiveImportL1ReactiveImportL2ReactiveImportL3ReactiveExportReactiveExportT1ReactiveExportT2ReactiveExportL1ReactiveExportL2ReactiveExportL3DCCurrentDCVoltageDCPowerHeatSinkTempDCCurrentS1DCVoltageS1DCPowerS1DCEnergyS1DCCurrentS2DCVoltageS2DCPowerS2DCEnergyS2DCCurrentS3DCVoltageS3DCPowerS3DCEnergyS3DCCurrentS4DCVoltageS4DCPowerS4DCEnergyS4ChargeStateBatteryVoltagePhaseAngle" +const _MeasurementName = "FrequencyCurrentCurrentL1CurrentL2CurrentL3VoltageVoltageL1VoltageL2VoltageL3PowerPowerL1PowerL2PowerL3ImportPowerImportPowerL1ImportPowerL2ImportPowerL3ExportPowerExportPowerL1ExportPowerL2ExportPowerL3ReactivePowerReactivePowerL1ReactivePowerL2ReactivePowerL3ApparentPowerApparentPowerL1ApparentPowerL2ApparentPowerL3CosphiCosphiL1CosphiL2CosphiL3THDTHDL1THDL2THDL3SumSumT1SumT2SumL1SumL2SumL3ImportImportT1ImportT2ImportL1ImportL2ImportL3ExportExportT1ExportT2ExportL1ExportL2ExportL3ReactiveSumReactiveSumT1ReactiveSumT2ReactiveSumL1ReactiveSumL2ReactiveSumL3ReactiveImportReactiveImportT1ReactiveImportT2ReactiveImportL1ReactiveImportL2ReactiveImportL3ReactiveExportReactiveExportT1ReactiveExportT2ReactiveExportL1ReactiveExportL2ReactiveExportL3DCCurrentDCVoltageDCPowerHeatSinkTempCabinetTempDCCurrentS1DCVoltageS1DCPowerS1DCEnergyS1DCCurrentS2DCVoltageS2DCPowerS2DCEnergyS2DCCurrentS3DCVoltageS3DCPowerS3DCEnergyS3DCCurrentS4DCVoltageS4DCPowerS4DCEnergyS4ChargeStateBatteryVoltagePhaseAngleStatusStatusVendor" -var _MeasurementIndex = [...]uint16{0, 9, 16, 25, 34, 43, 50, 59, 68, 77, 82, 89, 96, 103, 114, 127, 140, 153, 164, 177, 190, 203, 216, 231, 246, 261, 274, 289, 304, 319, 325, 333, 341, 349, 352, 357, 362, 367, 370, 375, 380, 385, 390, 395, 401, 409, 417, 425, 433, 441, 447, 455, 463, 471, 479, 487, 498, 511, 524, 537, 550, 563, 577, 593, 609, 625, 641, 657, 671, 687, 703, 719, 735, 751, 760, 769, 776, 788, 799, 810, 819, 829, 840, 851, 860, 870, 881, 892, 901, 911, 922, 933, 942, 952, 963, 977, 987} +var _MeasurementIndex = [...]uint16{0, 9, 16, 25, 34, 43, 50, 59, 68, 77, 82, 89, 96, 103, 114, 127, 140, 153, 164, 177, 190, 203, 216, 231, 246, 261, 274, 289, 304, 319, 325, 333, 341, 349, 352, 357, 362, 367, 370, 375, 380, 385, 390, 395, 401, 409, 417, 425, 433, 441, 447, 455, 463, 471, 479, 487, 498, 511, 524, 537, 550, 563, 577, 593, 609, 625, 641, 657, 671, 687, 703, 719, 735, 751, 760, 769, 776, 788, 799, 810, 821, 830, 840, 851, 862, 871, 881, 892, 903, 912, 922, 933, 944, 953, 963, 974, 988, 998, 1004, 1016} -const _MeasurementLowerName = "frequencycurrentcurrentl1currentl2currentl3voltagevoltagel1voltagel2voltagel3powerpowerl1powerl2powerl3importpowerimportpowerl1importpowerl2importpowerl3exportpowerexportpowerl1exportpowerl2exportpowerl3reactivepowerreactivepowerl1reactivepowerl2reactivepowerl3apparentpowerapparentpowerl1apparentpowerl2apparentpowerl3cosphicosphil1cosphil2cosphil3thdthdl1thdl2thdl3sumsumt1sumt2suml1suml2suml3importimportt1importt2importl1importl2importl3exportexportt1exportt2exportl1exportl2exportl3reactivesumreactivesumt1reactivesumt2reactivesuml1reactivesuml2reactivesuml3reactiveimportreactiveimportt1reactiveimportt2reactiveimportl1reactiveimportl2reactiveimportl3reactiveexportreactiveexportt1reactiveexportt2reactiveexportl1reactiveexportl2reactiveexportl3dccurrentdcvoltagedcpowerheatsinktempdccurrents1dcvoltages1dcpowers1dcenergys1dccurrents2dcvoltages2dcpowers2dcenergys2dccurrents3dcvoltages3dcpowers3dcenergys3dccurrents4dcvoltages4dcpowers4dcenergys4chargestatebatteryvoltagephaseangle" +const _MeasurementLowerName = "frequencycurrentcurrentl1currentl2currentl3voltagevoltagel1voltagel2voltagel3powerpowerl1powerl2powerl3importpowerimportpowerl1importpowerl2importpowerl3exportpowerexportpowerl1exportpowerl2exportpowerl3reactivepowerreactivepowerl1reactivepowerl2reactivepowerl3apparentpowerapparentpowerl1apparentpowerl2apparentpowerl3cosphicosphil1cosphil2cosphil3thdthdl1thdl2thdl3sumsumt1sumt2suml1suml2suml3importimportt1importt2importl1importl2importl3exportexportt1exportt2exportl1exportl2exportl3reactivesumreactivesumt1reactivesumt2reactivesuml1reactivesuml2reactivesuml3reactiveimportreactiveimportt1reactiveimportt2reactiveimportl1reactiveimportl2reactiveimportl3reactiveexportreactiveexportt1reactiveexportt2reactiveexportl1reactiveexportl2reactiveexportl3dccurrentdcvoltagedcpowerheatsinktempcabinettempdccurrents1dcvoltages1dcpowers1dcenergys1dccurrents2dcvoltages2dcpowers2dcenergys2dccurrents3dcvoltages3dcpowers3dcenergys3dccurrents4dcvoltages4dcpowers4dcenergys4chargestatebatteryvoltagephaseanglestatusstatusvendor" func (i Measurement) String() string { i -= 1 @@ -102,222 +102,231 @@ func _MeasurementNoOp() { _ = x[DCVoltage-(75)] _ = x[DCPower-(76)] _ = x[HeatSinkTemp-(77)] - _ = x[DCCurrentS1-(78)] - _ = x[DCVoltageS1-(79)] - _ = x[DCPowerS1-(80)] - _ = x[DCEnergyS1-(81)] - _ = x[DCCurrentS2-(82)] - _ = x[DCVoltageS2-(83)] - _ = x[DCPowerS2-(84)] - _ = x[DCEnergyS2-(85)] - _ = x[DCCurrentS3-(86)] - _ = x[DCVoltageS3-(87)] - _ = x[DCPowerS3-(88)] - _ = x[DCEnergyS3-(89)] - _ = x[DCCurrentS4-(90)] - _ = x[DCVoltageS4-(91)] - _ = x[DCPowerS4-(92)] - _ = x[DCEnergyS4-(93)] - _ = x[ChargeState-(94)] - _ = x[BatteryVoltage-(95)] - _ = x[PhaseAngle-(96)] + _ = x[CabinetTemp-(78)] + _ = x[DCCurrentS1-(79)] + _ = x[DCVoltageS1-(80)] + _ = x[DCPowerS1-(81)] + _ = x[DCEnergyS1-(82)] + _ = x[DCCurrentS2-(83)] + _ = x[DCVoltageS2-(84)] + _ = x[DCPowerS2-(85)] + _ = x[DCEnergyS2-(86)] + _ = x[DCCurrentS3-(87)] + _ = x[DCVoltageS3-(88)] + _ = x[DCPowerS3-(89)] + _ = x[DCEnergyS3-(90)] + _ = x[DCCurrentS4-(91)] + _ = x[DCVoltageS4-(92)] + _ = x[DCPowerS4-(93)] + _ = x[DCEnergyS4-(94)] + _ = x[ChargeState-(95)] + _ = x[BatteryVoltage-(96)] + _ = x[PhaseAngle-(97)] + _ = x[Status-(98)] + _ = x[StatusVendor-(99)] } -var _MeasurementValues = []Measurement{Frequency, Current, CurrentL1, CurrentL2, CurrentL3, Voltage, VoltageL1, VoltageL2, VoltageL3, Power, PowerL1, PowerL2, PowerL3, ImportPower, ImportPowerL1, ImportPowerL2, ImportPowerL3, ExportPower, ExportPowerL1, ExportPowerL2, ExportPowerL3, ReactivePower, ReactivePowerL1, ReactivePowerL2, ReactivePowerL3, ApparentPower, ApparentPowerL1, ApparentPowerL2, ApparentPowerL3, Cosphi, CosphiL1, CosphiL2, CosphiL3, THD, THDL1, THDL2, THDL3, Sum, SumT1, SumT2, SumL1, SumL2, SumL3, Import, ImportT1, ImportT2, ImportL1, ImportL2, ImportL3, Export, ExportT1, ExportT2, ExportL1, ExportL2, ExportL3, ReactiveSum, ReactiveSumT1, ReactiveSumT2, ReactiveSumL1, ReactiveSumL2, ReactiveSumL3, ReactiveImport, ReactiveImportT1, ReactiveImportT2, ReactiveImportL1, ReactiveImportL2, ReactiveImportL3, ReactiveExport, ReactiveExportT1, ReactiveExportT2, ReactiveExportL1, ReactiveExportL2, ReactiveExportL3, DCCurrent, DCVoltage, DCPower, HeatSinkTemp, DCCurrentS1, DCVoltageS1, DCPowerS1, DCEnergyS1, DCCurrentS2, DCVoltageS2, DCPowerS2, DCEnergyS2, DCCurrentS3, DCVoltageS3, DCPowerS3, DCEnergyS3, DCCurrentS4, DCVoltageS4, DCPowerS4, DCEnergyS4, ChargeState, BatteryVoltage, PhaseAngle} +var _MeasurementValues = []Measurement{Frequency, Current, CurrentL1, CurrentL2, CurrentL3, Voltage, VoltageL1, VoltageL2, VoltageL3, Power, PowerL1, PowerL2, PowerL3, ImportPower, ImportPowerL1, ImportPowerL2, ImportPowerL3, ExportPower, ExportPowerL1, ExportPowerL2, ExportPowerL3, ReactivePower, ReactivePowerL1, ReactivePowerL2, ReactivePowerL3, ApparentPower, ApparentPowerL1, ApparentPowerL2, ApparentPowerL3, Cosphi, CosphiL1, CosphiL2, CosphiL3, THD, THDL1, THDL2, THDL3, Sum, SumT1, SumT2, SumL1, SumL2, SumL3, Import, ImportT1, ImportT2, ImportL1, ImportL2, ImportL3, Export, ExportT1, ExportT2, ExportL1, ExportL2, ExportL3, ReactiveSum, ReactiveSumT1, ReactiveSumT2, ReactiveSumL1, ReactiveSumL2, ReactiveSumL3, ReactiveImport, ReactiveImportT1, ReactiveImportT2, ReactiveImportL1, ReactiveImportL2, ReactiveImportL3, ReactiveExport, ReactiveExportT1, ReactiveExportT2, ReactiveExportL1, ReactiveExportL2, ReactiveExportL3, DCCurrent, DCVoltage, DCPower, HeatSinkTemp, CabinetTemp, DCCurrentS1, DCVoltageS1, DCPowerS1, DCEnergyS1, DCCurrentS2, DCVoltageS2, DCPowerS2, DCEnergyS2, DCCurrentS3, DCVoltageS3, DCPowerS3, DCEnergyS3, DCCurrentS4, DCVoltageS4, DCPowerS4, DCEnergyS4, ChargeState, BatteryVoltage, PhaseAngle, Status, StatusVendor} var _MeasurementNameToValueMap = map[string]Measurement{ - _MeasurementName[0:9]: Frequency, - _MeasurementLowerName[0:9]: Frequency, - _MeasurementName[9:16]: Current, - _MeasurementLowerName[9:16]: Current, - _MeasurementName[16:25]: CurrentL1, - _MeasurementLowerName[16:25]: CurrentL1, - _MeasurementName[25:34]: CurrentL2, - _MeasurementLowerName[25:34]: CurrentL2, - _MeasurementName[34:43]: CurrentL3, - _MeasurementLowerName[34:43]: CurrentL3, - _MeasurementName[43:50]: Voltage, - _MeasurementLowerName[43:50]: Voltage, - _MeasurementName[50:59]: VoltageL1, - _MeasurementLowerName[50:59]: VoltageL1, - _MeasurementName[59:68]: VoltageL2, - _MeasurementLowerName[59:68]: VoltageL2, - _MeasurementName[68:77]: VoltageL3, - _MeasurementLowerName[68:77]: VoltageL3, - _MeasurementName[77:82]: Power, - _MeasurementLowerName[77:82]: Power, - _MeasurementName[82:89]: PowerL1, - _MeasurementLowerName[82:89]: PowerL1, - _MeasurementName[89:96]: PowerL2, - _MeasurementLowerName[89:96]: PowerL2, - _MeasurementName[96:103]: PowerL3, - _MeasurementLowerName[96:103]: PowerL3, - _MeasurementName[103:114]: ImportPower, - _MeasurementLowerName[103:114]: ImportPower, - _MeasurementName[114:127]: ImportPowerL1, - _MeasurementLowerName[114:127]: ImportPowerL1, - _MeasurementName[127:140]: ImportPowerL2, - _MeasurementLowerName[127:140]: ImportPowerL2, - _MeasurementName[140:153]: ImportPowerL3, - _MeasurementLowerName[140:153]: ImportPowerL3, - _MeasurementName[153:164]: ExportPower, - _MeasurementLowerName[153:164]: ExportPower, - _MeasurementName[164:177]: ExportPowerL1, - _MeasurementLowerName[164:177]: ExportPowerL1, - _MeasurementName[177:190]: ExportPowerL2, - _MeasurementLowerName[177:190]: ExportPowerL2, - _MeasurementName[190:203]: ExportPowerL3, - _MeasurementLowerName[190:203]: ExportPowerL3, - _MeasurementName[203:216]: ReactivePower, - _MeasurementLowerName[203:216]: ReactivePower, - _MeasurementName[216:231]: ReactivePowerL1, - _MeasurementLowerName[216:231]: ReactivePowerL1, - _MeasurementName[231:246]: ReactivePowerL2, - _MeasurementLowerName[231:246]: ReactivePowerL2, - _MeasurementName[246:261]: ReactivePowerL3, - _MeasurementLowerName[246:261]: ReactivePowerL3, - _MeasurementName[261:274]: ApparentPower, - _MeasurementLowerName[261:274]: ApparentPower, - _MeasurementName[274:289]: ApparentPowerL1, - _MeasurementLowerName[274:289]: ApparentPowerL1, - _MeasurementName[289:304]: ApparentPowerL2, - _MeasurementLowerName[289:304]: ApparentPowerL2, - _MeasurementName[304:319]: ApparentPowerL3, - _MeasurementLowerName[304:319]: ApparentPowerL3, - _MeasurementName[319:325]: Cosphi, - _MeasurementLowerName[319:325]: Cosphi, - _MeasurementName[325:333]: CosphiL1, - _MeasurementLowerName[325:333]: CosphiL1, - _MeasurementName[333:341]: CosphiL2, - _MeasurementLowerName[333:341]: CosphiL2, - _MeasurementName[341:349]: CosphiL3, - _MeasurementLowerName[341:349]: CosphiL3, - _MeasurementName[349:352]: THD, - _MeasurementLowerName[349:352]: THD, - _MeasurementName[352:357]: THDL1, - _MeasurementLowerName[352:357]: THDL1, - _MeasurementName[357:362]: THDL2, - _MeasurementLowerName[357:362]: THDL2, - _MeasurementName[362:367]: THDL3, - _MeasurementLowerName[362:367]: THDL3, - _MeasurementName[367:370]: Sum, - _MeasurementLowerName[367:370]: Sum, - _MeasurementName[370:375]: SumT1, - _MeasurementLowerName[370:375]: SumT1, - _MeasurementName[375:380]: SumT2, - _MeasurementLowerName[375:380]: SumT2, - _MeasurementName[380:385]: SumL1, - _MeasurementLowerName[380:385]: SumL1, - _MeasurementName[385:390]: SumL2, - _MeasurementLowerName[385:390]: SumL2, - _MeasurementName[390:395]: SumL3, - _MeasurementLowerName[390:395]: SumL3, - _MeasurementName[395:401]: Import, - _MeasurementLowerName[395:401]: Import, - _MeasurementName[401:409]: ImportT1, - _MeasurementLowerName[401:409]: ImportT1, - _MeasurementName[409:417]: ImportT2, - _MeasurementLowerName[409:417]: ImportT2, - _MeasurementName[417:425]: ImportL1, - _MeasurementLowerName[417:425]: ImportL1, - _MeasurementName[425:433]: ImportL2, - _MeasurementLowerName[425:433]: ImportL2, - _MeasurementName[433:441]: ImportL3, - _MeasurementLowerName[433:441]: ImportL3, - _MeasurementName[441:447]: Export, - _MeasurementLowerName[441:447]: Export, - _MeasurementName[447:455]: ExportT1, - _MeasurementLowerName[447:455]: ExportT1, - _MeasurementName[455:463]: ExportT2, - _MeasurementLowerName[455:463]: ExportT2, - _MeasurementName[463:471]: ExportL1, - _MeasurementLowerName[463:471]: ExportL1, - _MeasurementName[471:479]: ExportL2, - _MeasurementLowerName[471:479]: ExportL2, - _MeasurementName[479:487]: ExportL3, - _MeasurementLowerName[479:487]: ExportL3, - _MeasurementName[487:498]: ReactiveSum, - _MeasurementLowerName[487:498]: ReactiveSum, - _MeasurementName[498:511]: ReactiveSumT1, - _MeasurementLowerName[498:511]: ReactiveSumT1, - _MeasurementName[511:524]: ReactiveSumT2, - _MeasurementLowerName[511:524]: ReactiveSumT2, - _MeasurementName[524:537]: ReactiveSumL1, - _MeasurementLowerName[524:537]: ReactiveSumL1, - _MeasurementName[537:550]: ReactiveSumL2, - _MeasurementLowerName[537:550]: ReactiveSumL2, - _MeasurementName[550:563]: ReactiveSumL3, - _MeasurementLowerName[550:563]: ReactiveSumL3, - _MeasurementName[563:577]: ReactiveImport, - _MeasurementLowerName[563:577]: ReactiveImport, - _MeasurementName[577:593]: ReactiveImportT1, - _MeasurementLowerName[577:593]: ReactiveImportT1, - _MeasurementName[593:609]: ReactiveImportT2, - _MeasurementLowerName[593:609]: ReactiveImportT2, - _MeasurementName[609:625]: ReactiveImportL1, - _MeasurementLowerName[609:625]: ReactiveImportL1, - _MeasurementName[625:641]: ReactiveImportL2, - _MeasurementLowerName[625:641]: ReactiveImportL2, - _MeasurementName[641:657]: ReactiveImportL3, - _MeasurementLowerName[641:657]: ReactiveImportL3, - _MeasurementName[657:671]: ReactiveExport, - _MeasurementLowerName[657:671]: ReactiveExport, - _MeasurementName[671:687]: ReactiveExportT1, - _MeasurementLowerName[671:687]: ReactiveExportT1, - _MeasurementName[687:703]: ReactiveExportT2, - _MeasurementLowerName[687:703]: ReactiveExportT2, - _MeasurementName[703:719]: ReactiveExportL1, - _MeasurementLowerName[703:719]: ReactiveExportL1, - _MeasurementName[719:735]: ReactiveExportL2, - _MeasurementLowerName[719:735]: ReactiveExportL2, - _MeasurementName[735:751]: ReactiveExportL3, - _MeasurementLowerName[735:751]: ReactiveExportL3, - _MeasurementName[751:760]: DCCurrent, - _MeasurementLowerName[751:760]: DCCurrent, - _MeasurementName[760:769]: DCVoltage, - _MeasurementLowerName[760:769]: DCVoltage, - _MeasurementName[769:776]: DCPower, - _MeasurementLowerName[769:776]: DCPower, - _MeasurementName[776:788]: HeatSinkTemp, - _MeasurementLowerName[776:788]: HeatSinkTemp, - _MeasurementName[788:799]: DCCurrentS1, - _MeasurementLowerName[788:799]: DCCurrentS1, - _MeasurementName[799:810]: DCVoltageS1, - _MeasurementLowerName[799:810]: DCVoltageS1, - _MeasurementName[810:819]: DCPowerS1, - _MeasurementLowerName[810:819]: DCPowerS1, - _MeasurementName[819:829]: DCEnergyS1, - _MeasurementLowerName[819:829]: DCEnergyS1, - _MeasurementName[829:840]: DCCurrentS2, - _MeasurementLowerName[829:840]: DCCurrentS2, - _MeasurementName[840:851]: DCVoltageS2, - _MeasurementLowerName[840:851]: DCVoltageS2, - _MeasurementName[851:860]: DCPowerS2, - _MeasurementLowerName[851:860]: DCPowerS2, - _MeasurementName[860:870]: DCEnergyS2, - _MeasurementLowerName[860:870]: DCEnergyS2, - _MeasurementName[870:881]: DCCurrentS3, - _MeasurementLowerName[870:881]: DCCurrentS3, - _MeasurementName[881:892]: DCVoltageS3, - _MeasurementLowerName[881:892]: DCVoltageS3, - _MeasurementName[892:901]: DCPowerS3, - _MeasurementLowerName[892:901]: DCPowerS3, - _MeasurementName[901:911]: DCEnergyS3, - _MeasurementLowerName[901:911]: DCEnergyS3, - _MeasurementName[911:922]: DCCurrentS4, - _MeasurementLowerName[911:922]: DCCurrentS4, - _MeasurementName[922:933]: DCVoltageS4, - _MeasurementLowerName[922:933]: DCVoltageS4, - _MeasurementName[933:942]: DCPowerS4, - _MeasurementLowerName[933:942]: DCPowerS4, - _MeasurementName[942:952]: DCEnergyS4, - _MeasurementLowerName[942:952]: DCEnergyS4, - _MeasurementName[952:963]: ChargeState, - _MeasurementLowerName[952:963]: ChargeState, - _MeasurementName[963:977]: BatteryVoltage, - _MeasurementLowerName[963:977]: BatteryVoltage, - _MeasurementName[977:987]: PhaseAngle, - _MeasurementLowerName[977:987]: PhaseAngle, + _MeasurementName[0:9]: Frequency, + _MeasurementLowerName[0:9]: Frequency, + _MeasurementName[9:16]: Current, + _MeasurementLowerName[9:16]: Current, + _MeasurementName[16:25]: CurrentL1, + _MeasurementLowerName[16:25]: CurrentL1, + _MeasurementName[25:34]: CurrentL2, + _MeasurementLowerName[25:34]: CurrentL2, + _MeasurementName[34:43]: CurrentL3, + _MeasurementLowerName[34:43]: CurrentL3, + _MeasurementName[43:50]: Voltage, + _MeasurementLowerName[43:50]: Voltage, + _MeasurementName[50:59]: VoltageL1, + _MeasurementLowerName[50:59]: VoltageL1, + _MeasurementName[59:68]: VoltageL2, + _MeasurementLowerName[59:68]: VoltageL2, + _MeasurementName[68:77]: VoltageL3, + _MeasurementLowerName[68:77]: VoltageL3, + _MeasurementName[77:82]: Power, + _MeasurementLowerName[77:82]: Power, + _MeasurementName[82:89]: PowerL1, + _MeasurementLowerName[82:89]: PowerL1, + _MeasurementName[89:96]: PowerL2, + _MeasurementLowerName[89:96]: PowerL2, + _MeasurementName[96:103]: PowerL3, + _MeasurementLowerName[96:103]: PowerL3, + _MeasurementName[103:114]: ImportPower, + _MeasurementLowerName[103:114]: ImportPower, + _MeasurementName[114:127]: ImportPowerL1, + _MeasurementLowerName[114:127]: ImportPowerL1, + _MeasurementName[127:140]: ImportPowerL2, + _MeasurementLowerName[127:140]: ImportPowerL2, + _MeasurementName[140:153]: ImportPowerL3, + _MeasurementLowerName[140:153]: ImportPowerL3, + _MeasurementName[153:164]: ExportPower, + _MeasurementLowerName[153:164]: ExportPower, + _MeasurementName[164:177]: ExportPowerL1, + _MeasurementLowerName[164:177]: ExportPowerL1, + _MeasurementName[177:190]: ExportPowerL2, + _MeasurementLowerName[177:190]: ExportPowerL2, + _MeasurementName[190:203]: ExportPowerL3, + _MeasurementLowerName[190:203]: ExportPowerL3, + _MeasurementName[203:216]: ReactivePower, + _MeasurementLowerName[203:216]: ReactivePower, + _MeasurementName[216:231]: ReactivePowerL1, + _MeasurementLowerName[216:231]: ReactivePowerL1, + _MeasurementName[231:246]: ReactivePowerL2, + _MeasurementLowerName[231:246]: ReactivePowerL2, + _MeasurementName[246:261]: ReactivePowerL3, + _MeasurementLowerName[246:261]: ReactivePowerL3, + _MeasurementName[261:274]: ApparentPower, + _MeasurementLowerName[261:274]: ApparentPower, + _MeasurementName[274:289]: ApparentPowerL1, + _MeasurementLowerName[274:289]: ApparentPowerL1, + _MeasurementName[289:304]: ApparentPowerL2, + _MeasurementLowerName[289:304]: ApparentPowerL2, + _MeasurementName[304:319]: ApparentPowerL3, + _MeasurementLowerName[304:319]: ApparentPowerL3, + _MeasurementName[319:325]: Cosphi, + _MeasurementLowerName[319:325]: Cosphi, + _MeasurementName[325:333]: CosphiL1, + _MeasurementLowerName[325:333]: CosphiL1, + _MeasurementName[333:341]: CosphiL2, + _MeasurementLowerName[333:341]: CosphiL2, + _MeasurementName[341:349]: CosphiL3, + _MeasurementLowerName[341:349]: CosphiL3, + _MeasurementName[349:352]: THD, + _MeasurementLowerName[349:352]: THD, + _MeasurementName[352:357]: THDL1, + _MeasurementLowerName[352:357]: THDL1, + _MeasurementName[357:362]: THDL2, + _MeasurementLowerName[357:362]: THDL2, + _MeasurementName[362:367]: THDL3, + _MeasurementLowerName[362:367]: THDL3, + _MeasurementName[367:370]: Sum, + _MeasurementLowerName[367:370]: Sum, + _MeasurementName[370:375]: SumT1, + _MeasurementLowerName[370:375]: SumT1, + _MeasurementName[375:380]: SumT2, + _MeasurementLowerName[375:380]: SumT2, + _MeasurementName[380:385]: SumL1, + _MeasurementLowerName[380:385]: SumL1, + _MeasurementName[385:390]: SumL2, + _MeasurementLowerName[385:390]: SumL2, + _MeasurementName[390:395]: SumL3, + _MeasurementLowerName[390:395]: SumL3, + _MeasurementName[395:401]: Import, + _MeasurementLowerName[395:401]: Import, + _MeasurementName[401:409]: ImportT1, + _MeasurementLowerName[401:409]: ImportT1, + _MeasurementName[409:417]: ImportT2, + _MeasurementLowerName[409:417]: ImportT2, + _MeasurementName[417:425]: ImportL1, + _MeasurementLowerName[417:425]: ImportL1, + _MeasurementName[425:433]: ImportL2, + _MeasurementLowerName[425:433]: ImportL2, + _MeasurementName[433:441]: ImportL3, + _MeasurementLowerName[433:441]: ImportL3, + _MeasurementName[441:447]: Export, + _MeasurementLowerName[441:447]: Export, + _MeasurementName[447:455]: ExportT1, + _MeasurementLowerName[447:455]: ExportT1, + _MeasurementName[455:463]: ExportT2, + _MeasurementLowerName[455:463]: ExportT2, + _MeasurementName[463:471]: ExportL1, + _MeasurementLowerName[463:471]: ExportL1, + _MeasurementName[471:479]: ExportL2, + _MeasurementLowerName[471:479]: ExportL2, + _MeasurementName[479:487]: ExportL3, + _MeasurementLowerName[479:487]: ExportL3, + _MeasurementName[487:498]: ReactiveSum, + _MeasurementLowerName[487:498]: ReactiveSum, + _MeasurementName[498:511]: ReactiveSumT1, + _MeasurementLowerName[498:511]: ReactiveSumT1, + _MeasurementName[511:524]: ReactiveSumT2, + _MeasurementLowerName[511:524]: ReactiveSumT2, + _MeasurementName[524:537]: ReactiveSumL1, + _MeasurementLowerName[524:537]: ReactiveSumL1, + _MeasurementName[537:550]: ReactiveSumL2, + _MeasurementLowerName[537:550]: ReactiveSumL2, + _MeasurementName[550:563]: ReactiveSumL3, + _MeasurementLowerName[550:563]: ReactiveSumL3, + _MeasurementName[563:577]: ReactiveImport, + _MeasurementLowerName[563:577]: ReactiveImport, + _MeasurementName[577:593]: ReactiveImportT1, + _MeasurementLowerName[577:593]: ReactiveImportT1, + _MeasurementName[593:609]: ReactiveImportT2, + _MeasurementLowerName[593:609]: ReactiveImportT2, + _MeasurementName[609:625]: ReactiveImportL1, + _MeasurementLowerName[609:625]: ReactiveImportL1, + _MeasurementName[625:641]: ReactiveImportL2, + _MeasurementLowerName[625:641]: ReactiveImportL2, + _MeasurementName[641:657]: ReactiveImportL3, + _MeasurementLowerName[641:657]: ReactiveImportL3, + _MeasurementName[657:671]: ReactiveExport, + _MeasurementLowerName[657:671]: ReactiveExport, + _MeasurementName[671:687]: ReactiveExportT1, + _MeasurementLowerName[671:687]: ReactiveExportT1, + _MeasurementName[687:703]: ReactiveExportT2, + _MeasurementLowerName[687:703]: ReactiveExportT2, + _MeasurementName[703:719]: ReactiveExportL1, + _MeasurementLowerName[703:719]: ReactiveExportL1, + _MeasurementName[719:735]: ReactiveExportL2, + _MeasurementLowerName[719:735]: ReactiveExportL2, + _MeasurementName[735:751]: ReactiveExportL3, + _MeasurementLowerName[735:751]: ReactiveExportL3, + _MeasurementName[751:760]: DCCurrent, + _MeasurementLowerName[751:760]: DCCurrent, + _MeasurementName[760:769]: DCVoltage, + _MeasurementLowerName[760:769]: DCVoltage, + _MeasurementName[769:776]: DCPower, + _MeasurementLowerName[769:776]: DCPower, + _MeasurementName[776:788]: HeatSinkTemp, + _MeasurementLowerName[776:788]: HeatSinkTemp, + _MeasurementName[788:799]: CabinetTemp, + _MeasurementLowerName[788:799]: CabinetTemp, + _MeasurementName[799:810]: DCCurrentS1, + _MeasurementLowerName[799:810]: DCCurrentS1, + _MeasurementName[810:821]: DCVoltageS1, + _MeasurementLowerName[810:821]: DCVoltageS1, + _MeasurementName[821:830]: DCPowerS1, + _MeasurementLowerName[821:830]: DCPowerS1, + _MeasurementName[830:840]: DCEnergyS1, + _MeasurementLowerName[830:840]: DCEnergyS1, + _MeasurementName[840:851]: DCCurrentS2, + _MeasurementLowerName[840:851]: DCCurrentS2, + _MeasurementName[851:862]: DCVoltageS2, + _MeasurementLowerName[851:862]: DCVoltageS2, + _MeasurementName[862:871]: DCPowerS2, + _MeasurementLowerName[862:871]: DCPowerS2, + _MeasurementName[871:881]: DCEnergyS2, + _MeasurementLowerName[871:881]: DCEnergyS2, + _MeasurementName[881:892]: DCCurrentS3, + _MeasurementLowerName[881:892]: DCCurrentS3, + _MeasurementName[892:903]: DCVoltageS3, + _MeasurementLowerName[892:903]: DCVoltageS3, + _MeasurementName[903:912]: DCPowerS3, + _MeasurementLowerName[903:912]: DCPowerS3, + _MeasurementName[912:922]: DCEnergyS3, + _MeasurementLowerName[912:922]: DCEnergyS3, + _MeasurementName[922:933]: DCCurrentS4, + _MeasurementLowerName[922:933]: DCCurrentS4, + _MeasurementName[933:944]: DCVoltageS4, + _MeasurementLowerName[933:944]: DCVoltageS4, + _MeasurementName[944:953]: DCPowerS4, + _MeasurementLowerName[944:953]: DCPowerS4, + _MeasurementName[953:963]: DCEnergyS4, + _MeasurementLowerName[953:963]: DCEnergyS4, + _MeasurementName[963:974]: ChargeState, + _MeasurementLowerName[963:974]: ChargeState, + _MeasurementName[974:988]: BatteryVoltage, + _MeasurementLowerName[974:988]: BatteryVoltage, + _MeasurementName[988:998]: PhaseAngle, + _MeasurementLowerName[988:998]: PhaseAngle, + _MeasurementName[998:1004]: Status, + _MeasurementLowerName[998:1004]: Status, + _MeasurementName[1004:1016]: StatusVendor, + _MeasurementLowerName[1004:1016]: StatusVendor, } var _MeasurementNames = []string{ @@ -400,23 +409,26 @@ var _MeasurementNames = []string{ _MeasurementName[776:788], _MeasurementName[788:799], _MeasurementName[799:810], - _MeasurementName[810:819], - _MeasurementName[819:829], - _MeasurementName[829:840], + _MeasurementName[810:821], + _MeasurementName[821:830], + _MeasurementName[830:840], _MeasurementName[840:851], - _MeasurementName[851:860], - _MeasurementName[860:870], - _MeasurementName[870:881], + _MeasurementName[851:862], + _MeasurementName[862:871], + _MeasurementName[871:881], _MeasurementName[881:892], - _MeasurementName[892:901], - _MeasurementName[901:911], - _MeasurementName[911:922], + _MeasurementName[892:903], + _MeasurementName[903:912], + _MeasurementName[912:922], _MeasurementName[922:933], - _MeasurementName[933:942], - _MeasurementName[942:952], - _MeasurementName[952:963], - _MeasurementName[963:977], - _MeasurementName[977:987], + _MeasurementName[933:944], + _MeasurementName[944:953], + _MeasurementName[953:963], + _MeasurementName[963:974], + _MeasurementName[974:988], + _MeasurementName[988:998], + _MeasurementName[998:1004], + _MeasurementName[1004:1016], } // MeasurementString retrieves an enum value from the enum constants string name. diff --git a/meters/measurements.go b/meters/measurements.go index b863c266..eda3bacf 100644 --- a/meters/measurements.go +++ b/meters/measurements.go @@ -2,7 +2,11 @@ package meters import ( "fmt" + "log" + "strings" "time" + + "github.com/volkszaehler/mbmd/meters/units" ) // MeasurementResult is the result of modbus read operation @@ -17,14 +21,12 @@ func (r MeasurementResult) String() string { return fmt.Sprintf("%s: %.2f%s", r.Measurement.String(), r.Value, unit) } -// Measurement is the type of measurement, i.e. the physical property being measued in common notation +// Measurement is the type of measurement, i.e. the physical property being measured in common notation type Measurement int -//go:generate enumer -type=Measurement +//go:generate go run github.com/dmarkham/enumer -type=Measurement const ( - _ Measurement = iota - - Frequency + Frequency Measurement = iota + 1 Current CurrentL1 @@ -120,6 +122,7 @@ const ( DCVoltage DCPower HeatSinkTemp + CabinetTemp // Strings DCCurrentS1 @@ -144,130 +147,329 @@ const ( BatteryVoltage PhaseAngle + + // Status + Status + StatusVendor ) -var iec = map[Measurement][]string{ - Frequency: {"Frequency", "Hz"}, - Current: {"Current", "A"}, - CurrentL1: {"L1 Current", "A"}, - CurrentL2: {"L2 Current", "A"}, - CurrentL3: {"L3 Current", "A"}, - Voltage: {"Voltage", "V"}, - VoltageL1: {"L1 Voltage", "V"}, - VoltageL2: {"L2 Voltage", "V"}, - VoltageL3: {"L3 Voltage", "V"}, - Power: {"Power", "W"}, - PowerL1: {"L1 Power", "W"}, - PowerL2: {"L2 Power", "W"}, - PowerL3: {"L3 Power", "W"}, - ImportPower: {"Import Power", "W"}, - ImportPowerL1: {"L1 Import Power", "W"}, - ImportPowerL2: {"L2 Import Power", "W"}, - ImportPowerL3: {"L3 Import Power", "W"}, - ExportPower: {"Export Power", "W"}, - ExportPowerL1: {"L1 Export Power", "W"}, - ExportPowerL2: {"L2 Export Power", "W"}, - ExportPowerL3: {"L3 Export Power", "W"}, - ReactivePower: {"Reactive Power", "var"}, - ReactivePowerL1: {"L1 Reactive Power", "var"}, - ReactivePowerL2: {"L2 Reactive Power", "var"}, - ReactivePowerL3: {"L3 Reactive Power", "var"}, - ApparentPower: {"Apparent Power", "VA"}, - ApparentPowerL1: {"L1 Apparent Power", "VA"}, - ApparentPowerL2: {"L2 Apparent Power", "VA"}, - ApparentPowerL3: {"L3 Apparent Power", "VA"}, - Cosphi: {"Cosphi"}, - CosphiL1: {"L1 Cosphi"}, - CosphiL2: {"L2 Cosphi"}, - CosphiL3: {"L3 Cosphi"}, - THD: {"Average voltage to neutral THD", "%"}, - THDL1: {"L1 Voltage to neutral THD", "%"}, - THDL2: {"L2 Voltage to neutral THD", "%"}, - THDL3: {"L3 Voltage to neutral THD", "%"}, - Sum: {"Total Sum", "kWh"}, - SumT1: {"Tariff 1 Sum", "kWh"}, - SumT2: {"Tariff 2 Sum", "kWh"}, - SumL1: {"L1 Sum", "kWh"}, - SumL2: {"L2 Sum", "kWh"}, - SumL3: {"L3 Sum", "kWh"}, - Import: {"Total Import", "kWh"}, - ImportT1: {"Tariff 1 Import", "kWh"}, - ImportT2: {"Tariff 2 Import", "kWh"}, - ImportL1: {"L1 Import", "kWh"}, - ImportL2: {"L2 Import", "kWh"}, - ImportL3: {"L3 Import", "kWh"}, - Export: {"Total Export", "kWh"}, - ExportT1: {"Tariff 1 Export", "kWh"}, - ExportT2: {"Tariff 2 Export", "kWh"}, - ExportL1: {"L1 Export", "kWh"}, - ExportL2: {"L2 Export", "kWh"}, - ExportL3: {"L3 Export", "kWh"}, - ReactiveSum: {"Total Reactive", "kvarh"}, - ReactiveSumT1: {"Tariff 1 Reactive", "kvarh"}, - ReactiveSumT2: {"Tariff 2 Reactive", "kvarh"}, - ReactiveSumL1: {"L1 Reactive", "kvarh"}, - ReactiveSumL2: {"L2 Reactive", "kvarh"}, - ReactiveSumL3: {"L3 Reactive", "kvarh"}, - ReactiveImport: {"Reactive Import", "kvarh"}, - ReactiveImportT1: {"Tariff 1 Reactive Import", "kvarh"}, - ReactiveImportT2: {"Tariff 2 Reactive Import", "kvarh"}, - ReactiveImportL1: {"L1 Reactive Import", "kvarh"}, - ReactiveImportL2: {"L2 Reactive Import", "kvarh"}, - ReactiveImportL3: {"L3 Reactive Import", "kvarh"}, - ReactiveExport: {"Reactive Export", "kvarh"}, - ReactiveExportT1: {"Tariff 1 Reactive Export", "kvarh"}, - ReactiveExportT2: {"Tariff 2 Reactive Export", "kvarh"}, - ReactiveExportL1: {"L1 Reactive Export", "kvarh"}, - ReactiveExportL2: {"L2 Reactive Export", "kvarh"}, - ReactiveExportL3: {"L3 Reactive Export", "kvarh"}, - DCCurrent: {"DC Current", "A"}, - DCVoltage: {"DC Voltage", "V"}, - DCPower: {"DC Power", "W"}, - HeatSinkTemp: {"Heat Sink Temperature", "°C"}, - DCCurrentS1: {"String 1 Current", "A"}, - DCVoltageS1: {"String 1 Voltage", "V"}, - DCPowerS1: {"String 1 Power", "W"}, - DCEnergyS1: {"String 1 Generation", "kWh"}, - DCCurrentS2: {"String 2 Current", "A"}, - DCVoltageS2: {"String 2 Voltage", "V"}, - DCPowerS2: {"String 2 Power", "W"}, - DCEnergyS2: {"String 2 Generation", "kWh"}, - DCCurrentS3: {"String 3 Current", "A"}, - DCVoltageS3: {"String 3 Voltage", "V"}, - DCPowerS3: {"String 3 Power", "W"}, - DCEnergyS3: {"String 3 Generation", "kWh"}, - DCCurrentS4: {"String 4 Current", "A"}, - DCVoltageS4: {"String 4 Voltage", "V"}, - DCPowerS4: {"String 4 Power", "W"}, - DCEnergyS4: {"String 4 Generation", "kWh"}, - ChargeState: {"Charge State", "%"}, - BatteryVoltage: {"Battery Voltage", "V"}, - PhaseAngle: {"Phase Angle", "°"}, +var iec = map[Measurement]*measurement{ + Frequency: newMeasurement(withDesc("Frequency"), withPrometheusHelpText("Frequency of the power line in Hertz"), withUnit(units.Hertz), withMetricType(Gauge)), + Current: newMeasurement(withDesc("Current"), withUnit(units.Ampere), withMetricType(Gauge)), + CurrentL1: newMeasurement(withDesc("L1 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + CurrentL2: newMeasurement(withDesc("L2 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + CurrentL3: newMeasurement(withDesc("L3 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + Voltage: newMeasurement(withDesc("Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + VoltageL1: newMeasurement(withDesc("L1 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + VoltageL2: newMeasurement(withDesc("L2 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + VoltageL3: newMeasurement(withDesc("L3 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + Power: newMeasurement(withDesc("Power"), withUnit(units.Watt), withMetricType(Gauge)), + PowerL1: newMeasurement(withDesc("L1 Power"), withUnit(units.Watt), withMetricType(Gauge)), + PowerL2: newMeasurement(withDesc("L2 Power"), withUnit(units.Watt), withMetricType(Gauge)), + PowerL3: newMeasurement(withDesc("L3 Power"), withUnit(units.Watt), withMetricType(Gauge)), + ImportPower: newMeasurement(withDesc("Import Power"), withUnit(units.Watt), withMetricType(Gauge)), + ImportPowerL1: newMeasurement(withDesc("L1 Import Power"), withUnit(units.Watt), withMetricType(Gauge)), + ImportPowerL2: newMeasurement(withDesc("L2 Import Power"), withUnit(units.Watt), withMetricType(Gauge)), + ImportPowerL3: newMeasurement(withDesc("L3 Import Power"), withUnit(units.Watt), withMetricType(Gauge)), + ExportPower: newMeasurement(withDesc("Export Power"), withUnit(units.Watt), withMetricType(Gauge)), + ExportPowerL1: newMeasurement(withDesc("L1 Export Power"), withUnit(units.Watt), withMetricType(Gauge)), + ExportPowerL2: newMeasurement(withDesc("L2 Export Power"), withUnit(units.Watt), withMetricType(Gauge)), + ExportPowerL3: newMeasurement(withDesc("L3 Export Power"), withUnit(units.Watt), withMetricType(Gauge)), + ReactivePower: newMeasurement(withDesc("Reactive Power"), withUnit(units.Var), withMetricType(Gauge)), + ReactivePowerL1: newMeasurement(withDesc("L1 Reactive Power"), withUnit(units.Var), withMetricType(Gauge)), + ReactivePowerL2: newMeasurement(withDesc("L2 Reactive Power"), withUnit(units.Var), withMetricType(Gauge)), + ReactivePowerL3: newMeasurement(withDesc("L3 Reactive Power"), withUnit(units.Var), withMetricType(Gauge)), + ApparentPower: newMeasurement(withDesc("Apparent Power"), withUnit(units.Voltampere), withMetricType(Gauge)), + ApparentPowerL1: newMeasurement(withDesc("L1 Apparent Power"), withUnit(units.Voltampere), withMetricType(Gauge)), + ApparentPowerL2: newMeasurement(withDesc("L2 Apparent Power"), withUnit(units.Voltampere), withMetricType(Gauge)), + ApparentPowerL3: newMeasurement(withDesc("L3 Apparent Power"), withUnit(units.Voltampere), withMetricType(Gauge)), + Cosphi: newMeasurement(withDesc("Power Factor Cosphi"), withMetricType(Gauge)), + CosphiL1: newMeasurement(withDesc("L1 Power Factor Cosphi"), withMetricType(Gauge)), + CosphiL2: newMeasurement(withDesc("L2 Power Factor Cosphi"), withMetricType(Gauge)), + CosphiL3: newMeasurement(withDesc("L3 Power Factor Cosphi"), withMetricType(Gauge)), + THD: newMeasurement(withDesc("Average voltage to neutral THD"), withUnit(units.Percent), withMetricType(Gauge)), + THDL1: newMeasurement(withDesc("L1 Voltage to neutral THD"), withUnit(units.Percent), withMetricType(Gauge)), + THDL2: newMeasurement(withDesc("L2 Voltage to neutral THD"), withUnit(units.Percent), withMetricType(Gauge)), + THDL3: newMeasurement(withDesc("L3 Voltage to neutral THD"), withUnit(units.Percent), withMetricType(Gauge)), + + Sum: newMeasurement(withDesc("Total Energy Sum"), withPromName("energy_sum"), withUnit(units.KiloWattHour), withMetricType(Counter)), + SumT1: newMeasurement(withDesc("Tariff 1 Energy Sum"), withUnit(units.KiloWattHour), withMetricType(Counter)), + SumT2: newMeasurement(withDesc("Tariff 2 Energy Sum"), withUnit(units.KiloWattHour), withMetricType(Counter)), + SumL1: newMeasurement(withDesc("L1 Energy Sum"), withUnit(units.KiloWattHour), withMetricType(Counter)), + SumL2: newMeasurement(withDesc("L2 Energy Sum"), withUnit(units.KiloWattHour), withMetricType(Counter)), + SumL3: newMeasurement(withDesc("L3 Energy Sum"), withUnit(units.KiloWattHour), withMetricType(Counter)), + Import: newMeasurement(withDesc("Total Import Energy"), withPromName("energy_imported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ImportT1: newMeasurement(withDesc("Tariff 1 Import Energy"), withPromName("tariff_1_energy_imported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ImportT2: newMeasurement(withDesc("Tariff 2 Import Energy"), withPromName("tariff_2_energy_imported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ImportL1: newMeasurement(withDesc("L1 Import Energy"), withPromName("l1_energy_imported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ImportL2: newMeasurement(withDesc("L2 Import Energy"), withPromName("l2_energy_imported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ImportL3: newMeasurement(withDesc("L3 Import Energy"), withPromName("l3_energy_imported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + Export: newMeasurement(withDesc("Total Export Energy"), withPromName("energy_exported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ExportT1: newMeasurement(withDesc("Tariff 1 Export Energy"), withPromName("tariff_1_energy_exported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ExportT2: newMeasurement(withDesc("Tariff 2 Export Energy"), withPromName("tariff_2_energy_exported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ExportL1: newMeasurement(withDesc("L1 Export Energy"), withPromName("l1_energy_exported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ExportL2: newMeasurement(withDesc("L2 Export Energy"), withPromName("l2_energy_exported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ExportL3: newMeasurement(withDesc("L3 Export Energy"), withPromName("l3_energy_exported"), withUnit(units.KiloWattHour), withMetricType(Counter)), + ReactiveSum: newMeasurement(withDesc("Total Reactive Energy"), withPromName("reactive_energy"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveSumT1: newMeasurement(withDesc("Tariff 1 Reactive Energy"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveSumT2: newMeasurement(withDesc("Tariff 2 Reactive Energy"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveSumL1: newMeasurement(withDesc("L1 Reactive Energy"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveSumL2: newMeasurement(withDesc("L2 Reactive Energy"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveSumL3: newMeasurement(withDesc("L3 Reactive Energy"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveImport: newMeasurement(withDesc("Reactive Import Energy"), withPromName("reactive_energy_imported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveImportT1: newMeasurement(withDesc("Tariff 1 Reactive Import Energy"), withPromName("tariff_2_reactive_energy_imported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveImportT2: newMeasurement(withDesc("Tariff 2 Reactive Import Energy"), withPromName("tariff_1_reactive_energy_imported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveImportL1: newMeasurement(withDesc("L1 Reactive Import Energy"), withPromName("l1_reactive_energy_imported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveImportL2: newMeasurement(withDesc("L2 Reactive Import Energy"), withPromName("l2_reactive_energy_imported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveImportL3: newMeasurement(withDesc("L3 Reactive Import Energy"), withPromName("l3_reactive_energy_imported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveExport: newMeasurement(withDesc("Reactive Export Energy"), withPromName("reactive_energy_exported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveExportT1: newMeasurement(withDesc("Tariff 1 Reactive Export Energy"), withPromName("tariff_1_reactive_energy_exported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveExportT2: newMeasurement(withDesc("Tariff 2 Reactive Export Energy"), withPromName("tariff_2_reactive_energy_exported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveExportL1: newMeasurement(withDesc("L1 Reactive Export Energy"), withPromName("l1_reactive_energy_exported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveExportL2: newMeasurement(withDesc("L2 Reactive Export Energy"), withPromName("l2_reactive_energy_exported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + ReactiveExportL3: newMeasurement(withDesc("L3 Reactive Export Energy"), withPromName("l3_reactive_energy_exported"), withUnit(units.KiloVarHour), withMetricType(Counter)), + DCCurrent: newMeasurement(withDesc("DC Current"), withUnit(units.Ampere), withMetricType(Gauge)), + DCVoltage: newMeasurement(withDesc("DC Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + DCPower: newMeasurement(withDesc("DC Power"), withUnit(units.Watt), withMetricType(Gauge)), + HeatSinkTemp: newMeasurement(withDesc("Heat Sink Temperature"), withUnit(units.DegreeCelsius), withMetricType(Gauge)), + CabinetTemp: newMeasurement(withDesc("Cabinet Temperature"), withUnit(units.DegreeCelsius), withMetricType(Gauge)), + DCCurrentS1: newMeasurement(withDesc("String 1 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + DCVoltageS1: newMeasurement(withDesc("String 1 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + DCPowerS1: newMeasurement(withDesc("String 1 Power"), withUnit(units.Watt), withMetricType(Gauge)), + DCEnergyS1: newMeasurement(withDesc("String 1 Generation"), withPromName("string_1_energy_generated"), withUnit(units.KiloWattHour), withMetricType(Counter)), + DCCurrentS2: newMeasurement(withDesc("String 2 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + DCVoltageS2: newMeasurement(withDesc("String 2 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + DCPowerS2: newMeasurement(withDesc("String 2 Power"), withUnit(units.Watt), withMetricType(Gauge)), + DCEnergyS2: newMeasurement(withDesc("String 2 Generation"), withPromName("string_2_energy_generated"), withUnit(units.KiloWattHour), withMetricType(Counter)), + DCCurrentS3: newMeasurement(withDesc("String 3 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + DCVoltageS3: newMeasurement(withDesc("String 3 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + DCPowerS3: newMeasurement(withDesc("String 3 Power"), withUnit(units.Watt), withMetricType(Gauge)), + DCEnergyS3: newMeasurement(withDesc("String 3 Generation"), withPromName("string_3_energy_generated"), withUnit(units.KiloWattHour), withMetricType(Counter)), + DCCurrentS4: newMeasurement(withDesc("String 4 Current"), withUnit(units.Ampere), withMetricType(Gauge)), + DCVoltageS4: newMeasurement(withDesc("String 4 Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + DCPowerS4: newMeasurement(withDesc("String 4 Power"), withUnit(units.Watt), withMetricType(Gauge)), + DCEnergyS4: newMeasurement(withDesc("String 4 Generation"), withUnit(units.KiloWattHour), withMetricType(Gauge)), + ChargeState: newMeasurement(withDesc("Charge State"), withUnit(units.Percent), withMetricType(Gauge)), + BatteryVoltage: newMeasurement(withDesc("Battery Voltage"), withUnit(units.Volt), withMetricType(Gauge)), + PhaseAngle: newMeasurement(withDesc("Phase Angle"), withUnit(units.Degree), withMetricType(Gauge)), + Status: newMeasurement(withDesc("Status"), withMetricType(Gauge)), // Operating State + StatusVendor: newMeasurement(withDesc("Status Vendor"), withMetricType(Gauge)), // Vendor-defined operating state and error codes. } // MarshalText implements encoding.TextMarshaler -func (m *Measurement) MarshalText() (text []byte, err error) { +func (m Measurement) MarshalText() ([]byte, error) { return []byte(m.String()), nil } // DescriptionAndUnit returns a measurements human-readable name and its unit -func (m *Measurement) DescriptionAndUnit() (string, string) { - if details, ok := iec[*m]; ok { - unit := "" - description := details[0] - if len(details) > 1 { - unit = details[1] - } - return description, unit +func (m Measurement) DescriptionAndUnit() (string, string) { + if details, ok := iec[m]; ok { + unit := details.Unit + description := details.Description + return description, unit.Abbreviation() } return m.String(), "" } +func (m Measurement) Unit() units.Unit { + if details, ok := iec[m]; ok { + return details.Unit + } + + return 0 +} + // Description returns a measurements human-readable name -func (m *Measurement) Description() string { +func (m Measurement) Description() string { description, unit := m.DescriptionAndUnit() if unit != "" { description = description + " (" + unit + ")" } return description } + +// PrometheusMetricType returns the Measurement's associated prometheus.Metric type +func (m Measurement) PrometheusMetricType() MetricType { + if measurement, ok := iec[m]; ok { + return measurement.PrometheusInfo.MetricType + } + return 0 +} + +// PrometheusHelpText returns a description text appropriate for prometheus.Metric +func (m Measurement) PrometheusHelpText() string { + if measurement, ok := iec[m]; ok { + return measurement.PrometheusInfo.HelpText + } + return "" +} + +// PrometheusName returns a name and its associated unit for Prometheus counters0 +func (m Measurement) PrometheusName() string { + if details, ok := iec[m]; ok { + return details.PrometheusInfo.Name + } + return "" +} + +// measurement describes a Measurement itself, its unit and according prometheus.Metric type +// A measurement object is built by using the builder function newMeasurement. +// +// A Prometheus name and help text is "auto-generated". The format is: +// ::= measurement_[_] +// ::= | +// ::= // Elementary unit! +// ::= "total" // if metric type is Counter +// E. g.: +// +// newMeasurement(withDesc("Frequency Test With Some Text"), withUnit(Hertz), withMetricType(Gauge)/*Counter*/) +// => Name (before creating prometheus.Metric): "measurement_frequency_test_with_some_text_hertz_total" +// => Description: "Frequency Test With Some Text in Hertz" +// +// In Prometheus context: If Unit is set, then it will be automatically converted to its elementary unit. +// +// (see units.ConvertValueToElementaryUnit) +// +// You can set custom Prometheus names and help texts by using the measurementOptions +// to override the "auto-generated" name and help text +// - withPromName +// - withPrometheusHelpText +// However, please make sure that the custom name conforms to Prometheus' naming conventions. +// (See https://prometheus.io/docs/practices/naming/) +// Please also note that PrometheusInfo.Name does not equal the actual name of prometheus.Metric; +// It's a partial name that will be concatenated together with a globally defined namespace (and for measurements with `measurement`) +// (see also prometheus.CreateMeasurementMetrics and generatePrometheusName) +type measurement struct { + Description string + Unit units.Unit + PrometheusInfo *PrometheusInfo +} + +// measurementOptions are used in newMeasurement +type measurementOptions func(*measurement) + +// PrometheusInfo carries Prometheus relevant information for e. g. creating metrics +type PrometheusInfo struct { + Name string + HelpText string + MetricType MetricType + Unit units.Unit +} + +// MetricType is the type of a measurement's prometheus.Metric to be used +type MetricType int + +const ( + Gauge MetricType = iota + 1 + Counter +) + +// newMeasurement generates an internal measurement object based on passed options +// +// If one of the following options are not passed: +// - withDesc +// - withMetricType +// +// the app will panic! +func newMeasurement(opts ...measurementOptions) *measurement { + m := &measurement{ + PrometheusInfo: &PrometheusInfo{}, + } + + for _, opt := range opts { + opt(m) + } + + if m.Description == "" || m.PrometheusInfo.MetricType == 0 { + log.Fatalf( + "Cannot create internal `measurement` because either Description or MetricType is empty."+ + "(Description: %v, MetricType: %v)", + m.Description, + m.PrometheusInfo.MetricType, + ) + } + + if m.Unit == 0 { + withUnit(units.NoUnit)(m) + } + + if m.PrometheusInfo.HelpText == "" { + withGenericPrometheusHelpText()(m) + } + + if m.PrometheusInfo.Name == "" { + m.PrometheusInfo.Name = generatePrometheusName(m.Description, m.PrometheusInfo.MetricType) + } else { + m.PrometheusInfo.Name = generatePrometheusName(m.PrometheusInfo.Name, m.PrometheusInfo.MetricType) + } + + return m +} + +// withPrometheusHelpText enables setting a Prometheus description of a Measurement +func withPrometheusHelpText(description string) measurementOptions { + return func(m *measurement) { + m.PrometheusInfo.HelpText = description + } +} + +// withGenericPrometheusHelpText sets the Prometheus description to a generated, more generic format +func withGenericPrometheusHelpText() measurementOptions { + return func(m *measurement) { + m.PrometheusInfo.HelpText = generatePrometheusHelpText(m.Description, m.PrometheusInfo.Unit) + } +} + +// withUnit sets the Unit of a Measurement +// If u is nil, the unit will be set to NoUnit +func withUnit(u units.Unit) measurementOptions { + return func(m *measurement) { + m.Unit = u + m.PrometheusInfo.Unit = m.Unit + } +} + +func withPromName(name string) measurementOptions { + return func(m *measurement) { + m.PrometheusInfo.Name = name + } +} + +func withMetricType(metricType MetricType) measurementOptions { + return func(m *measurement) { + m.PrometheusInfo.MetricType = metricType + } +} + +func withDesc(description string) measurementOptions { + return func(m *measurement) { + m.Description = description + } +} + +func generatePrometheusHelpText(description string, unit units.Unit) string { + if unit > 0 && unit < units.NoUnit { + _, pluralForm := unit.Name() + return fmt.Sprintf("%s in %s", description, pluralForm) + } + return description +} + +func generatePrometheusName(name string, metricType MetricType) string { + measurementName := strings.ToLower(name) + + measurementName = strings.Trim(strings.ReplaceAll(measurementName, " ", "_"), "_") + + var counterSuffix string + if metricType == Counter { + counterSuffix = "total" + } + + return strings.Trim( // Trim trailing underscore (e. g. when unit string is empty) + strings.Join( + []string{"measurement", measurementName, counterSuffix}, + "_", + ), + "_", + ) +} diff --git a/meters/measurements_test.go b/meters/measurements_test.go new file mode 100644 index 00000000..c870db1e --- /dev/null +++ b/meters/measurements_test.go @@ -0,0 +1,110 @@ +package meters + +import ( + "testing" + + "github.com/volkszaehler/mbmd/meters/units" +) + +func TestMeasurementCreation_WithRequiredOptions_WithMetricType_Counter(t *testing.T) { + measurement := newMeasurement( + withDesc("My Test Measurement"), + withUnit(units.Ampere), + withMetricType(Counter), + ) + + expectedPrometheusName := "measurement_my_test_measurement_total" + expectedDescription := "My Test Measurement in Amperes" + + if measurement.PrometheusInfo.Name != expectedPrometheusName { + t.Errorf( + "Prometheus metric name '%s' does not equal expected '%s'", + measurement.PrometheusInfo.Name, + expectedPrometheusName, + ) + } + + if measurement.PrometheusInfo.HelpText != expectedDescription { + t.Errorf("Prometheus description '%s' does not equal expected '%s'", + measurement.PrometheusInfo.HelpText, + expectedDescription, + ) + } + + if measurement.Unit != units.Ampere { + t.Errorf("Prometheus unit '%s' does not equal expected '%s'", + measurement.Unit, + units.Ampere, + ) + } +} + +func TestMeasurementCreation_WithCustomName_AndDescription(t *testing.T) { + measurement := newMeasurement( + withDesc("My Test Measurement"), + withPrometheusHelpText("My custom description for my measurement"), + withPromName("my_custom_name_for_my_test_measurement"), + withUnit(units.Ampere), + withMetricType(Gauge), + ) + + expectedPrometheusName := "measurement_my_custom_name_for_my_test_measurement" + expectedDescription := "My custom description for my measurement" + + if measurement.PrometheusInfo.Name != expectedPrometheusName { + t.Errorf( + "Prometheus metric name '%s' does not equal expected '%s'", + measurement.PrometheusInfo.Name, + expectedPrometheusName, + ) + } + + if measurement.PrometheusInfo.HelpText != expectedDescription { + t.Errorf("Prometheus description '%s' does not equal expected '%s'", + measurement.PrometheusInfo.HelpText, + expectedDescription, + ) + } +} + +func TestInternalMeasurement_AutoConvertToElementaryUnit(t *testing.T) { + measurementKwh := newMeasurement( + withDesc("My Test Measurement with kWh"), + withPrometheusHelpText("My custom description for my measurement"), + withPromName("my_custom_name_for_my_test_measurement_energy"), + withUnit(units.KiloWattHour), + withMetricType(Gauge), + ) + + measurementKvarh := newMeasurement( + withDesc("My Test Measurement"), + withPrometheusHelpText("My custom description for my measurement"), + withPromName("my_custom_name_for_my_test_measurement_energy"), + withUnit(units.KiloWattHour), + withMetricType(Gauge), + ) + + expectedConvertedUnit := units.KiloWattHour + _, expectedConvertedUnitPluralForm := expectedConvertedUnit.Name() + + if measurementKwh.PrometheusInfo.Unit != expectedConvertedUnit { + actualConvertedUnit := measurementKwh.PrometheusInfo.Unit + _, actualConvertedUnitPluralForm := actualConvertedUnit.Name() + + t.Errorf( + "measurement_kWh could not be converted to elementary unit '%s' automatically (actual: %s)", + expectedConvertedUnitPluralForm, + actualConvertedUnitPluralForm, + ) + } + + if measurementKvarh.PrometheusInfo.Unit != expectedConvertedUnit { + actualConvertedUnit := measurementKwh.PrometheusInfo.Unit + _, actualConvertedUnitPluralForm := actualConvertedUnit.Name() + + t.Errorf("measurement_kvarh could not be converted to elementary unit '%s' automatically (actual: %s)", + expectedConvertedUnitPluralForm, + actualConvertedUnitPluralForm, + ) + } +} diff --git a/meters/rs485/orno1p.go b/meters/rs485/orno1p.go index ba4b26d8..9add02a2 100644 --- a/meters/rs485/orno1p.go +++ b/meters/rs485/orno1p.go @@ -44,14 +44,14 @@ func NewORNO1PProducer() Producer { ApparentPower: 0x156, // 32 bit, 0.001kva Cosphi: 0x15B, // 16 bit, 0.001 - Sum: 0xA000, //32 Bit, 0.01kwh - SumT1: 0xA002, //32 Bit, 0.01kwh - SumT2: 0xA004, //32 Bit, 0.01kwh + Sum: 0xA000, // 32 Bit, 0.01kwh + SumT1: 0xA002, // 32 Bit, 0.01kwh + SumT2: 0xA004, // 32 Bit, 0.01kwh // SumT3: 0xA006, //32 Bit, 0.01kwh // currently not supported // SumT4: 0xA008, //32 Bit, 0.01kwh // currently not supported - ReactiveSum: 0xA01E, //32 Bit, 0.01kvarh - ReactiveSumT1: 0xA020, //32 Bit, 0.01kvarh - ReactiveSumT2: 0xA022, //32 Bit, 0.01kvarh + ReactiveSum: 0xA01E, // 32 Bit, 0.01kvarh + ReactiveSumT1: 0xA020, // 32 Bit, 0.01kvarh + ReactiveSumT2: 0xA022, // 32 Bit, 0.01kvarh // ReactiveSumT3: 0xA024, //32 Bit, 0.01kvarh // currently not supported // ReactiveSumT4: 0xA026, //32 Bit, 0.01kvarh // currently not supported } @@ -104,7 +104,6 @@ func (p *ORNO1PProducer) Probe() Operation { // Produce implements Producer interface func (p *ORNO1PProducer) Produce() (res []Operation) { - for _, op := range []Measurement{ VoltageL1, Frequency, diff --git a/meters/rs485/orno1p504.go b/meters/rs485/orno1p504.go index 57ed105f..40badb59 100644 --- a/meters/rs485/orno1p504.go +++ b/meters/rs485/orno1p504.go @@ -7,7 +7,6 @@ func init() { } var ops1p504 Opcodes = Opcodes{ - Frequency: 0x02, // 16 bit, Hz Voltage: 0x00, // 16 bit, V Current: 0x01, // 16 bit, A @@ -16,8 +15,8 @@ var ops1p504 Opcodes = Opcodes{ ApparentPower: 0x05, // 16 bit, va Cosphi: 0x06, // 16 bit, - Sum: 0x07, //32 Bit, wh - ReactiveSum: 0x09, //32 Bit, varh + Sum: 0x07, // 32 Bit, wh + ReactiveSum: 0x09, // 32 Bit, varh } type ORNO1P504Producer struct { diff --git a/meters/rs485/orno3p.go b/meters/rs485/orno3p.go index 1b8e5713..31397f12 100644 --- a/meters/rs485/orno3p.go +++ b/meters/rs485/orno3p.go @@ -44,49 +44,49 @@ func NewORNO3PProducer() Producer { ApparentPower: 0x002C, // 32 bit, kva Cosphi: 0x0034, // 32 bit, XX,X(literal) - Sum: 0x0100, //32 Bit, kwh - SumL1: 0x0102, //32 Bit, kwh - SumL2: 0x0104, //32 Bit, kwh - SumL3: 0x0106, //32 Bit, kwh - - Import: 0x0108, //32 Bit, kwh - ImportL1: 0x010A, //32 Bit, kwh - ImportL2: 0x010C, //32 Bit, kwh - ImportL3: 0x010E, //32 Bit, kwh - - Export: 0x0110, //32 Bit, kwh - ExportL1: 0x0112, //32 Bit, kwh - ExportL2: 0x0114, //32 Bit, kwh - ExportL3: 0x0116, //32 Bit, kwh - - ReactiveSum: 0x0118, //32 Bit, kvarh - ReactiveSumL1: 0x011A, //32 Bit, kvarh - ReactiveSumL2: 0x011C, //32 Bit, kvarh - ReactiveSumL3: 0x011E, //32 Bit, kvarh - - ReactiveImport: 0x0120, //32 Bit, kvarh - ReactiveImportL1: 0x0122, //32 Bit, kvarh - ReactiveImportL2: 0x0124, //32 Bit, kvarh - ReactiveImportL3: 0x0126, //32 Bit, kvarh - - ReactiveExport: 0x0128, //32 Bit, kvarh - ReactiveExportL1: 0x012A, //32 Bit, kvarh - ReactiveExportL2: 0x012C, //32 Bit, kvarh - ReactiveExportL3: 0x012E, //32 Bit, kvarh - - SumT1: 0x0130, //32 Bit, kwh - ImportT1: 0x0132, //32 Bit, kwh - ExportT1: 0x0134, //32 Bit, kwh - ReactiveSumT1: 0x0136, //32 Bit, kvarh - ReactiveImportT1: 0x0138, //32 Bit, kvarh - ReactiveExportT1: 0x013A, //32 Bit, kvarh - - SumT2: 0x013C, //32 Bit, kwh - ImportT2: 0x013E, //32 Bit, kwh - ExportT2: 0x0140, //32 Bit, kwh - ReactiveSumT2: 0x0142, //32 Bit, kvarh - ReactiveImportT2: 0x0144, //32 Bit, kvarh - ReactiveExportT2: 0x0146, //32 Bit, kvarh + Sum: 0x0100, // 32 Bit, kwh + SumL1: 0x0102, // 32 Bit, kwh + SumL2: 0x0104, // 32 Bit, kwh + SumL3: 0x0106, // 32 Bit, kwh + + Import: 0x0108, // 32 Bit, kwh + ImportL1: 0x010A, // 32 Bit, kwh + ImportL2: 0x010C, // 32 Bit, kwh + ImportL3: 0x010E, // 32 Bit, kwh + + Export: 0x0110, // 32 Bit, kwh + ExportL1: 0x0112, // 32 Bit, kwh + ExportL2: 0x0114, // 32 Bit, kwh + ExportL3: 0x0116, // 32 Bit, kwh + + ReactiveSum: 0x0118, // 32 Bit, kvarh + ReactiveSumL1: 0x011A, // 32 Bit, kvarh + ReactiveSumL2: 0x011C, // 32 Bit, kvarh + ReactiveSumL3: 0x011E, // 32 Bit, kvarh + + ReactiveImport: 0x0120, // 32 Bit, kvarh + ReactiveImportL1: 0x0122, // 32 Bit, kvarh + ReactiveImportL2: 0x0124, // 32 Bit, kvarh + ReactiveImportL3: 0x0126, // 32 Bit, kvarh + + ReactiveExport: 0x0128, // 32 Bit, kvarh + ReactiveExportL1: 0x012A, // 32 Bit, kvarh + ReactiveExportL2: 0x012C, // 32 Bit, kvarh + ReactiveExportL3: 0x012E, // 32 Bit, kvarh + + SumT1: 0x0130, // 32 Bit, kwh + ImportT1: 0x0132, // 32 Bit, kwh + ExportT1: 0x0134, // 32 Bit, kwh + ReactiveSumT1: 0x0136, // 32 Bit, kvarh + ReactiveImportT1: 0x0138, // 32 Bit, kvarh + ReactiveExportT1: 0x013A, // 32 Bit, kvarh + + SumT2: 0x013C, // 32 Bit, kwh + ImportT2: 0x013E, // 32 Bit, kwh + ExportT2: 0x0140, // 32 Bit, kwh + ReactiveSumT2: 0x0142, // 32 Bit, kvarh + ReactiveImportT2: 0x0144, // 32 Bit, kvarh + ReactiveExportT2: 0x0146, // 32 Bit, kvarh /* // Curently not supported SumT3: 0x0148, //32 Bit, kwh diff --git a/meters/rs485/rs485.go b/meters/rs485/rs485.go index 2fcdda40..6904c09b 100644 --- a/meters/rs485/rs485.go +++ b/meters/rs485/rs485.go @@ -16,17 +16,19 @@ const ( // RS485 implements meters.Device type RS485 struct { + name string typ string producer Producer ops chan Operation inflight Operation } -// NewDevice creates a device who's type must exist in the producer registry -func NewDevice(typ string) (*RS485, error) { +// NewDevice creates a device whose type must exist in the producer registry. +func NewDevice(name, typ string) (*RS485, error) { for t, factory := range Producers { if strings.EqualFold(t, typ) { device := &RS485{ + name: name, typ: typ, producer: factory(), } @@ -34,7 +36,7 @@ func NewDevice(typ string) (*RS485, error) { } } - return nil, fmt.Errorf("unknown meter type: %s", typ) + return nil, fmt.Errorf("unknown meter type: %s with name: %s", typ, name) } // Initialize prepares the device for usage. Any setup or initialization should be done here. @@ -51,6 +53,7 @@ func (d *RS485) Producer() Producer { // prepared during initialization. func (d *RS485) Descriptor() meters.DeviceDescriptor { return meters.DeviceDescriptor{ + Name: d.name, Type: d.typ, Manufacturer: d.typ, Model: d.producer.Description(), diff --git a/meters/rs485/sdm.go b/meters/rs485/sdm.go index a3431a62..98da8082 100644 --- a/meters/rs485/sdm.go +++ b/meters/rs485/sdm.go @@ -52,11 +52,11 @@ func NewSDMProducer() Producer { THDL3: 0x00ee, // voltage THD: 0x00F8, // voltage Frequency: 0x0046, // 230 - //L1THDCurrent: 0x00F0, // current - //L2THDCurrent: 0x00F2, // current - //L3THDCurrent: 0x00F4, // current - //AvgTHDCurrent: 0x00Fa, // current - //ApparentImportPower: 0x0064, + // L1THDCurrent: 0x00F0, // current + // L2THDCurrent: 0x00F2, // current + // L3THDCurrent: 0x00F4, // current + // AvgTHDCurrent: 0x00Fa, // current + // ApparentImportPower: 0x0064, } return &SDMProducer{Opcodes: ops} } diff --git a/meters/sunspec/models.go b/meters/sunspec/models.go index 9cca140f..d4b8159b 100644 --- a/meters/sunspec/models.go +++ b/meters/sunspec/models.go @@ -33,7 +33,8 @@ var modelMap = map[sunspec.ModelId]map[int]map[string]meters.Measurement{ model101.DCA: meters.DCCurrent, model101.DCV: meters.DCVoltage, model101.DCW: meters.DCPower, - model101.TmpCab: meters.HeatSinkTemp, + model101.TmpCab: meters.CabinetTemp, + model101.TmpSnk: meters.HeatSinkTemp, }, }, // single phase inverter - float @@ -51,7 +52,8 @@ var modelMap = map[sunspec.ModelId]map[int]map[string]meters.Measurement{ model111.DCA: meters.DCCurrent, model111.DCV: meters.DCVoltage, model111.DCW: meters.DCPower, - model111.TmpCab: meters.HeatSinkTemp, + model111.TmpCab: meters.CabinetTemp, + model111.TmpSnk: meters.HeatSinkTemp, }, }, // three phase inverter @@ -73,7 +75,10 @@ var modelMap = map[sunspec.ModelId]map[int]map[string]meters.Measurement{ model103.DCA: meters.DCCurrent, model103.DCV: meters.DCVoltage, model103.DCW: meters.DCPower, - model103.TmpCab: meters.HeatSinkTemp, + model103.TmpSnk: meters.HeatSinkTemp, + model103.TmpCab: meters.CabinetTemp, + model103.St: meters.Status, + model103.StVnd: meters.StatusVendor, }, }, // three phase inverter - float @@ -95,7 +100,10 @@ var modelMap = map[sunspec.ModelId]map[int]map[string]meters.Measurement{ model113.DCA: meters.DCCurrent, model113.DCV: meters.DCVoltage, model113.DCW: meters.DCPower, - model113.TmpCab: meters.HeatSinkTemp, + model113.TmpSnk: meters.HeatSinkTemp, + model113.TmpCab: meters.CabinetTemp, + model113.St: meters.Status, + model113.StVnd: meters.StatusVendor, }, }, model160.ModelID: { diff --git a/meters/sunspec/sunspec.go b/meters/sunspec/sunspec.go index e2071569..c486ed3f 100644 --- a/meters/sunspec/sunspec.go +++ b/meters/sunspec/sunspec.go @@ -10,11 +10,11 @@ import ( sunspec "github.com/andig/gosunspec" sunspecbus "github.com/andig/gosunspec/modbus" - "github.com/grid-x/modbus" - _ "github.com/andig/gosunspec/models" // device tree parsing requires all models "github.com/andig/gosunspec/models/model1" "github.com/andig/gosunspec/models/model101" + "github.com/andig/gosunspec/typelabel" + "github.com/grid-x/modbus" "github.com/volkszaehler/mbmd/meters" ) @@ -59,7 +59,7 @@ func DeviceTree(client modbus.Client) ([]sunspec.Device, error) { } // NewDevice creates a Sunspec device -func NewDevice(meterType string, subdevice ...int) *SunSpec { +func NewDevice(name string, meterType string, subdevice ...int) *SunSpec { var dev int if len(subdevice) > 0 { dev = subdevice[0] @@ -68,6 +68,7 @@ func NewDevice(meterType string, subdevice ...int) *SunSpec { return &SunSpec{ subdevice: dev, descriptor: meters.DeviceDescriptor{ + Name: name, Type: meterType, Manufacturer: meterType, SubDevice: dev, @@ -86,6 +87,8 @@ func (d *SunSpec) Initialize(client modbus.Client) error { return err } + // TODO prometheus: else DeviceModbusConnectionSuccess + // this may be ErrPartiallyOpened return err } @@ -144,7 +147,7 @@ func (d *SunSpec) collectModels(device sunspec.Device) error { func (d *SunSpec) relevantModelIds() []sunspec.ModelId { modelIds := make([]sunspec.ModelId, 0, len(modelMap)) for k := range modelMap { - modelIds = append(modelIds, sunspec.ModelId(k)) + modelIds = append(modelIds, k) } return modelIds @@ -214,6 +217,17 @@ func (d *SunSpec) convertPoint(b sunspec.Block, p sunspec.Point) (float64, error FixKostal(p) } + switch p.Type() { + case typelabel.Enum16: + return float64(p.Enum16()), nil + case typelabel.Enum32: + return float64(p.Enum32()), nil + case typelabel.Bitfield16: + return float64(p.Bitfield16()), nil + case typelabel.Bitfield32: + return float64(p.Bitfield32()), nil + } + v := p.ScaledValue() if math.IsNaN(v) { @@ -295,27 +309,27 @@ func (d *SunSpec) QueryOp(client modbus.Client, measurement meters.Measurement) } for _, model := range d.models { - for modelID, blockMap := range modelMap { - if modelID != model.Id() { + modelID := model.Id() + blockMap, ok := modelMap[modelID] + if !ok { + continue + } + + for blockID, pointMap := range blockMap { + if blockID >= model.Blocks() { continue } - for blockID, pointMap := range blockMap { - if blockID >= model.Blocks() { - continue - } - - for pointID, m := range pointMap { - if m == measurement { - v, err := d.QueryPoint(client, int(modelID), blockID, pointID) + for pointID, m := range pointMap { + if m == measurement { + v, err := d.QueryPoint(client, int(modelID), blockID, pointID) - var mr meters.MeasurementResult - if err == nil { - mr = makeResult(v, measurement) - } - - return mr, err + var mr meters.MeasurementResult + if err == nil { + mr = makeResult(v, measurement) } + + return mr, err } } } @@ -332,44 +346,44 @@ func (d *SunSpec) Query(client modbus.Client) (res []meters.MeasurementResult, e } for _, model := range d.models { - for modelID, blockMap := range modelMap { - if modelID != model.Id() { - continue - } + blockMap, ok := modelMap[model.Id()] + if !ok { + continue + } - // sort blocks so block 0 is always read first - sortedBlocks := make([]int, 0, len(blockMap)) - for k := range blockMap { - sortedBlocks = append(sortedBlocks, k) - } - sort.Ints(sortedBlocks) + // sort blocks so block 0 is always read first + sortedBlocks := make([]int, 0, len(blockMap)) + for k := range blockMap { + sortedBlocks = append(sortedBlocks, k) + } + sort.Ints(sortedBlocks) - // always add zero block - if sortedBlocks[0] != 0 { - sortedBlocks = append([]int{0}, sortedBlocks...) - } + // always add zero block + if sortedBlocks[0] != 0 { + sortedBlocks = append([]int{0}, sortedBlocks...) + } - for blockID := range sortedBlocks { - if blockID >= model.Blocks() { - continue - } + for blockID := range sortedBlocks { + if blockID >= model.Blocks() { + continue + } - pointMap := blockMap[blockID] - block := model.MustBlock(blockID) + pointMap := blockMap[blockID] + block := model.MustBlock(blockID) - if err := block.Read(); err != nil { - return res, err - } + if err := block.Read(); err != nil { + return res, err + } - for pointID, m := range pointMap { - point := block.MustPoint(pointID) + for pointID, m := range pointMap { + point := block.MustPoint(pointID) - if v, err := d.convertPoint(block, point); err == nil { - res = append(res, makeResult(v, m)) - } + if v, err := d.convertPoint(block, point); err == nil { + res = append(res, makeResult(v, m)) } } } + } return res, nil diff --git a/meters/units/unit_enumer.go b/meters/units/unit_enumer.go new file mode 100644 index 00000000..b6dde865 --- /dev/null +++ b/meters/units/unit_enumer.go @@ -0,0 +1,119 @@ +// Code generated by "enumer -type=Unit -transform=snake"; DO NOT EDIT. + +package units + +import ( + "fmt" + "strings" +) + +const _UnitName = "kilo_var_hourkilo_watt_houramperevoltwattvoltamperevardegreedegree_celsiushertzpercentno_unit" + +var _UnitIndex = [...]uint8{0, 13, 27, 33, 37, 41, 51, 54, 60, 74, 79, 86, 93} + +const _UnitLowerName = "kilo_var_hourkilo_watt_houramperevoltwattvoltamperevardegreedegree_celsiushertzpercentno_unit" + +func (i Unit) String() string { + i -= 1 + if i < 0 || i >= Unit(len(_UnitIndex)-1) { + return fmt.Sprintf("Unit(%d)", i+1) + } + return _UnitName[_UnitIndex[i]:_UnitIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _UnitNoOp() { + var x [1]struct{} + _ = x[KiloVarHour-(1)] + _ = x[KiloWattHour-(2)] + _ = x[Ampere-(3)] + _ = x[Volt-(4)] + _ = x[Watt-(5)] + _ = x[Voltampere-(6)] + _ = x[Var-(7)] + _ = x[Degree-(8)] + _ = x[DegreeCelsius-(9)] + _ = x[Hertz-(10)] + _ = x[Percent-(11)] + _ = x[NoUnit-(12)] +} + +var _UnitValues = []Unit{KiloVarHour, KiloWattHour, Ampere, Volt, Watt, Voltampere, Var, Degree, DegreeCelsius, Hertz, Percent, NoUnit} + +var _UnitNameToValueMap = map[string]Unit{ + _UnitName[0:13]: KiloVarHour, + _UnitLowerName[0:13]: KiloVarHour, + _UnitName[13:27]: KiloWattHour, + _UnitLowerName[13:27]: KiloWattHour, + _UnitName[27:33]: Ampere, + _UnitLowerName[27:33]: Ampere, + _UnitName[33:37]: Volt, + _UnitLowerName[33:37]: Volt, + _UnitName[37:41]: Watt, + _UnitLowerName[37:41]: Watt, + _UnitName[41:51]: Voltampere, + _UnitLowerName[41:51]: Voltampere, + _UnitName[51:54]: Var, + _UnitLowerName[51:54]: Var, + _UnitName[54:60]: Degree, + _UnitLowerName[54:60]: Degree, + _UnitName[60:74]: DegreeCelsius, + _UnitLowerName[60:74]: DegreeCelsius, + _UnitName[74:79]: Hertz, + _UnitLowerName[74:79]: Hertz, + _UnitName[79:86]: Percent, + _UnitLowerName[79:86]: Percent, + _UnitName[86:93]: NoUnit, + _UnitLowerName[86:93]: NoUnit, +} + +var _UnitNames = []string{ + _UnitName[0:13], + _UnitName[13:27], + _UnitName[27:33], + _UnitName[33:37], + _UnitName[37:41], + _UnitName[41:51], + _UnitName[51:54], + _UnitName[54:60], + _UnitName[60:74], + _UnitName[74:79], + _UnitName[79:86], + _UnitName[86:93], +} + +// UnitString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func UnitString(s string) (Unit, error) { + if val, ok := _UnitNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _UnitNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Unit values", s) +} + +// UnitValues returns all values of the enum +func UnitValues() []Unit { + return _UnitValues +} + +// UnitStrings returns a slice of all String values of the enum +func UnitStrings() []string { + strs := make([]string, len(_UnitNames)) + copy(strs, _UnitNames) + return strs +} + +// IsAUnit returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Unit) IsAUnit() bool { + for _, v := range _UnitValues { + if i == v { + return true + } + } + return false +} diff --git a/meters/units/units.go b/meters/units/units.go new file mode 100644 index 00000000..a5540fa4 --- /dev/null +++ b/meters/units/units.go @@ -0,0 +1,214 @@ +package units + +// Unit is used to represent measurements +type Unit int + +//go:generate go run github.com/dmarkham/enumer -type=Unit -transform=snake +const ( + KiloVarHour Unit = iota + 1 + KiloWattHour + + Ampere + Volt + + Watt + Voltampere + Var + + Degree + DegreeCelsius + + Hertz + + Percent + + NoUnit // max value +) + +type unit struct { + abbreviation unitAbbreviation + name unitName +} + +type internalUnitOption func(*unit) + +// unitAbbreviation defines the default abbreviation and - if needed - an alternative. +type unitAbbreviation struct { + Default string + Alternative string +} + +type unitName struct { + Singular string + Plural string + PrometheusForm string +} + +var units = map[Unit]*unit{ + KiloVarHour: newInternalUnit( + KiloVarHour, + withName("Kilovoltampere-hour (reactive)", "Kilovoltampere-hours (reactive)"), + withAbbreviation("kvarh", ""), + // Unit is automatically converted to Joules in Prometheus context! + ), + + KiloWattHour: newInternalUnit( + KiloWattHour, + withName("Kilowatt-hour", ""), + withAbbreviation("kWh", ""), + // Unit is automatically converted to Joules in Prometheus context! + ), + + Var: newInternalUnit( + Var, + withName("Voltampere (reactive)", "Voltamperes (reactive)"), + withAbbreviation(Var.String(), ""), + withNameInPrometheusForm("voltamperes"), + ), + + Watt: newInternalUnit( + Watt, + withName("Watt", ""), + withAbbreviation("W", ""), + ), + + Ampere: newInternalUnit( + Ampere, + withName("Ampere", ""), + withAbbreviation("A", ""), + ), + + Volt: newInternalUnit( + Volt, + withName("Volt", ""), + withAbbreviation("V", ""), + ), + + Voltampere: newInternalUnit( + Voltampere, + withName("Voltampere", ""), + withAbbreviation("VA", ""), + ), + + Degree: newInternalUnit( + Degree, + withName("Degree", ""), + withAbbreviation("°", "degree"), + ), + + DegreeCelsius: newInternalUnit( + DegreeCelsius, + withName("Degree Celsius", "Degrees Celsius"), + withAbbreviation("°C", "degree celsius"), + withNameInPrometheusForm("degrees_celsius"), + ), + + Hertz: newInternalUnit( + Hertz, + withName("Hertz", "Hertz"), + withAbbreviation("hz", ""), + withNameInPrometheusForm("hertz"), + ), + + Percent: newInternalUnit( + Percent, + withName("Percent", "Percent"), + withAbbreviation("%", "percent"), + withNameInPrometheusForm("percent"), + ), + + NoUnit: newInternalUnit(NoUnit), +} + +// PrometheusForm returns Prometheus form of the Unit's associated elementary unit +func (u Unit) PrometheusForm() string { + if u == 0 || u == NoUnit { + return "" + } + + if unit, ok := units[u]; ok { + return unit.name.PrometheusForm + } + + return "" +} + +// Abbreviation returns the matching abbreviation of a Unit if it exists +func (u Unit) Abbreviation() string { + if unit, ok := units[u]; ok { + return unit.abbreviation.Default + } + return "" +} + +// AlternativeAbbreviation returns the matching alternative abbreviation of a Unit if it exists +func (u Unit) AlternativeAbbreviation() string { + if unit, ok := units[u]; ok { + alternative := unit.abbreviation.Alternative + if alternative != "" { + return alternative + } + } + return "" +} + +// Name returns the singular and plural form of a Unit's name +func (u Unit) Name() (string, string) { + if unit, ok := units[u]; ok { + return unit.name.Singular, unit.name.Plural + } + return "", "" +} + +// newInternalUnit is a factory method for instantiating internal unit struct +func newInternalUnit(associatedUnit Unit, opts ...internalUnitOption) *unit { + unit := &unit{} + + // Early return for NoUnit as we do not want to specify anything for "nothing" + if associatedUnit == NoUnit { + return unit + } + + for _, opt := range opts { + opt(unit) + } + + if unit.name.PrometheusForm == "" { + unit.name.PrometheusForm = associatedUnit.String() + "s" + } + + if unit.name.Plural == "" { + unit.name.Plural = unit.name.Singular + "s" + } + + return unit +} + +// withAbbreviation defines a default and an alternative representation of the unit's abbreviation +// +// Leave `alternative` empty if not needed +func withAbbreviation(defaultAbbreviation string, alternative string) internalUnitOption { + return func(u *unit) { + u.abbreviation.Default = defaultAbbreviation + u.abbreviation.Alternative = alternative + } +} + +// withName defines the name of a unit - singular and plural +// +// Leave `plural` empty if an `s` can be appended to the literal `singular` string +func withName(singular string, plural string) internalUnitOption { + return func(u *unit) { + u.name.Singular = singular + u.name.Plural = plural + } +} + +// withNameInPrometheusForm sets an explicit Prometheus form for a unit +// +// You don't have to use this option if the associated Unit key with an appended `s` can be used +func withNameInPrometheusForm(prometheusForm string) internalUnitOption { + return func(u *unit) { + u.name.PrometheusForm = prometheusForm + } +} diff --git a/meters/units/units_test.go b/meters/units/units_test.go new file mode 100644 index 00000000..b5dd53d0 --- /dev/null +++ b/meters/units/units_test.go @@ -0,0 +1,42 @@ +package units + +import ( + "testing" +) + +func Test_makeInternalUnit(t *testing.T) { + internalUnit := newInternalUnit( + Ampere, + withName("Ampere", ""), + withAbbreviation("A", ""), + ) + + if internalUnit.name.Singular != "Ampere" { + t.Errorf( + "Actual defined singular name '%s' for Unit '%v' does not equal expected name '%s'", + internalUnit.name.Singular, + Ampere, + "Ampere", + ) + } + + if internalUnit.name.Plural != "Amperes" { + t.Errorf( + "Actual defined plural name '%s' for Unit '%v' does not equal expected name '%s'", + internalUnit.name.Plural, + Ampere, + "Amperes", + ) + } + + expectedPrometheusForm := Ampere.String() + "s" + + if internalUnit.name.PrometheusForm != expectedPrometheusForm { + t.Errorf( + "Actual defined Prometheus form '%s' for Unit '%v' does not equal expected name '%s'", + internalUnit.name.PrometheusForm, + Ampere, + expectedPrometheusForm, + ) + } +} diff --git a/prometheus/measurement_collector.go b/prometheus/measurement_collector.go new file mode 100644 index 00000000..3fc93b46 --- /dev/null +++ b/prometheus/measurement_collector.go @@ -0,0 +1,221 @@ +package prometheus + +import ( + "errors" + "log" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +// MeasurementCounterCollector is a struct which takes care of collecting prometheus.Counter metrics +// Whenever prometheus.Collect is called, all values of MeasurementCounterCollector.values are flushed +// and sent to the collector channel. +type MeasurementCounterCollector struct { + metricsMap *prometheus.MetricVec + desc *prometheus.Desc + values struct { + sync.RWMutex + data map[string]*measurementResult // Contains latest value of meters.MeasurementResult + } + fqName string + opts prometheus.CounterOpts +} + +func NewMeasurementCounterCollector(opts prometheus.CounterOpts) *MeasurementCounterCollector { + fqName := prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) + + collector := MeasurementCounterCollector{ + fqName: fqName, + opts: opts, + desc: prometheus.NewDesc( + fqName, + opts.Help, + measurementCollectorVariableLabels, + nil, + ), + } + collector.values.data = map[string]*measurementResult{} + collector.metricsMap = prometheus.NewMetricVec( + collector.desc, + collector.newMetric, + ) + + return &collector +} + +// Describe implements prometheus.Collector's Describe +func (c *MeasurementCounterCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.desc +} + +// Collect implements prometheus.Collector's Collect +func (c *MeasurementCounterCollector) Collect(ch chan<- prometheus.Metric) { + c.values.RLock() + defer c.values.RUnlock() + + for sKey, value := range c.values.data { + lvs := strings.Split(sKey, keySeparator) + + ch <- prometheus.NewMetricWithTimestamp(value.timestamp, + prometheus.MustNewConstMetric( + c.desc, + prometheus.CounterValue, + value.value, + lvs..., + ), + ) + } +} + +// Set sets the specified value for provided labelValues at a specified +// timestamp. value must be higher than 0 and higher than the previous value. +// Otherwise, en error will be logged. +// +// This function is thread-safe. +func (c *MeasurementCounterCollector) Set(timestamp time.Time, value float64, labelValues ...string) { + if value < 0 { + log.Println("[ERROR] counters cannot decrease in its value. ignoring.", c.fqName, "value", value) + return + } + + c.values.Lock() + defer c.values.Unlock() + + lvs := strings.Join(labelValues, keySeparator) + var prevValue float64 + if prev, ok := c.values.data[lvs]; ok && prev.value > 0 { + prevValue = prev.value + } + + if prevValue > 0 && value < prevValue { + log.Println("[WARN] counters cannot decrease in its value. ignoring.", c.fqName, "value", value, "prevValue", prevValue) + return + } + + c.values.data[lvs] = &measurementResult{ + value: value, + timestamp: timestamp, + } +} + +// MeasurementGaugeCollector is a struct which takes care of collecting +// prometheus.Gauge metrics Whenever prometheus.Collect is called, all values of +// MeasurementGaugeCollector.values are flushed and sent to the collector +// channel. +type MeasurementGaugeCollector struct { + metricsMap *prometheus.MetricVec + desc *prometheus.Desc + values struct { + sync.RWMutex + data map[string]*measurementResult // Contains latest value of meters.MeasurementResult + } + fqName string + opts prometheus.GaugeOpts +} + +func NewMeasurementGaugeCollector(opts prometheus.GaugeOpts) *MeasurementGaugeCollector { + fqName := prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) + + collector := MeasurementGaugeCollector{ + fqName: fqName, + opts: opts, + desc: prometheus.NewDesc( + fqName, + opts.Help, + measurementCollectorVariableLabels, + nil, + ), + } + collector.values.data = map[string]*measurementResult{} + collector.metricsMap = prometheus.NewMetricVec( + collector.desc, + collector.newMetric, + ) + + return &collector +} + +// Describe implements prometheus.Collector's Describe +func (c *MeasurementGaugeCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.desc +} + +// Collect implements prometheus.Collector's Collect +func (c *MeasurementGaugeCollector) Collect(ch chan<- prometheus.Metric) { + c.values.RLock() + defer c.values.RUnlock() + + for sKey, value := range c.values.data { + lvs := strings.Split(sKey, keySeparator) + + ch <- prometheus.NewMetricWithTimestamp(value.timestamp, + prometheus.MustNewConstMetric( + c.desc, + prometheus.GaugeValue, + value.value, + lvs..., + ), + ) + } +} + +// Set sets the specified value for provided labelValues at a specified timestamp. +// +// This function is thread-safe. +func (c *MeasurementGaugeCollector) Set(timestamp time.Time, value float64, labelValues ...string) { + c.values.Lock() + defer c.values.Unlock() + + lvs := strings.Join(labelValues, keySeparator) + c.values.data[lvs] = &measurementResult{ + value: value, + timestamp: timestamp, + } +} + +type measurementResult struct { + timestamp time.Time + value float64 +} + +// Separator used for concatenating label values. +const keySeparator = ";" + +// Copied from prometheus/labels.go for consistency purposes +var errInconsistentCardinality = errors.New("inconsistent label cardinality") + +// Labels for every measurement prometheus.CounterVec/ unit as e.g. C, F, hertz +var measurementCollectorVariableLabels = []string{"device_name", "serial_number", "unit"} + +type MetricFactory interface { + newMetric(lvs ...string) +} + +func (c *MeasurementCounterCollector) newMetric(lvs ...string) prometheus.Metric { + if len(lvs) != len(measurementCollectorVariableLabels) { + log.Fatalf( + "%s: %q has %d variable labels named %q but %d values %q were provided\n", + errInconsistentCardinality, c.fqName, + len(measurementCollectorVariableLabels), measurementCollectorVariableLabels, + len(lvs), lvs, + ) + } + + return prometheus.NewCounter(c.opts) +} + +func (c *MeasurementGaugeCollector) newMetric(lvs ...string) prometheus.Metric { + if len(lvs) != len(measurementCollectorVariableLabels) { + log.Fatalf( + "%s: %q has %d variable labels named %q but %d values %q were provided\n", + errInconsistentCardinality, c.fqName, + len(measurementCollectorVariableLabels), measurementCollectorVariableLabels, + len(lvs), lvs, + ) + } + + return prometheus.NewGauge(c.opts) +} diff --git a/prometheus/measurements.go b/prometheus/measurements.go new file mode 100644 index 00000000..c740b42b --- /dev/null +++ b/prometheus/measurements.go @@ -0,0 +1,81 @@ +package prometheus + +import ( + "github.com/volkszaehler/mbmd/meters" +) + +// These functions take care of registering and updating counters/gauges. +// General naming convention: `mbmd_['_total']` +// +// For instance: A counter for total connection attempts +// => Name for newCounterOpts: `smart_meter_connection_attempt_total` +// => Metric type: Counter +// => After prometheus.Metric creation: +// - Name: `mbmd_smart_meter_connection_attempt_total` +// - Metric: prometheus.Counter +// +// If a new measurement prometheus.Metric is created, it follows this convention: +// For instance: "L1 Export Power" with unit `W` +// => Name: `l1_export_power` +// => Metric type: Gauge +// => After prometheus.Metric creation: +// - Name: `mbmd_measurement_l1_export_power` +// - Labels: {"device_name", "serial_number", "unit"} +// +// Measurement metrics are treated slightly differently and are maintained in +// prometheus/measurement.go. It ensures that extensibility and customization of +// Prometheus names and help texts is easy. By default, if no custom Prometheus +// name is given, the measurement's description is transformed to lower case and +// whitespaces are replaced with underscores. Afterward, the measurement's +// elementary units.Unit and `total` (if meters.MetricType equals +// meters.Counter) are appended in a snake-case fashion. +// +// Besides dynamic measurement metrics, some static metrics have been introduced +// and can be found in the file respectively, for instance: metrics for devices +// -> devices.go +// +// If you want to add new metrics, make sure your metric details comply to the +// usual Prometheus naming conventions e.g.: Amount of connection attempts +// -> Most fitting metric type: Counter +// -> Name (in newCounterOpts): `smart_meter_connection_attempt_total` +// For more information regarding naming conventions and best practices, see +// https://prometheus.io/docs/practices/naming/ + +// SSN_MISSING is used for mocked smart meters +const SSN_MISSING = "NOT_AVAILABLE" + +// counterVecMap contains all meters.Measurement that are associated with a +// prometheus.Counter +// +// If a new meters.Measurement is introduced, it needs to be added either to +// counterVecMap or to gaugeVecMap - Otherwise Prometheus won't keep track of +// the newly added meters.Measurement +var counterVecMap = map[meters.Measurement]*MeasurementCounterCollector{} + +// gaugeVecMap contains all meters.Measurement that are associated with a +// prometheus.Gauge +// +// If a new meters.Measurement is introduced, it needs to be added either to +// counterVecMap or to gaugeVecMap - Otherwise Prometheus won't keep track of +// the newly added meters.Measurement +var gaugeVecMap = map[meters.Measurement]*MeasurementGaugeCollector{} + +// UpdateMeasurementMetric updates a counter or gauge based on passed measurement. +func UpdateMeasurementMetric( + deviceName string, + deviceSerial string, + measurement meters.MeasurementResult, +) { + // Handle empty device serial numbers (e.g. on mocks) + if deviceSerial == "" { + deviceSerial = SSN_MISSING + } + + if gauge, ok := gaugeVecMap[measurement.Measurement]; ok { + gauge.Set(measurement.Timestamp, measurement.Value, deviceName, deviceSerial, measurement.Unit().Abbreviation()) + return + } + if counter, ok := counterVecMap[measurement.Measurement]; ok { + counter.Set(measurement.Timestamp, measurement.Value, deviceName, deviceSerial, measurement.Unit().Abbreviation()) + } +} diff --git a/prometheus/metric_options.go b/prometheus/metric_options.go new file mode 100644 index 00000000..708a1614 --- /dev/null +++ b/prometheus/metric_options.go @@ -0,0 +1,25 @@ +package prometheus + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const NAMESPACE = "mbmd" + +// newCounterOpts creates a CounterOpts object, but with a predefined namespace. +func newCounterOpts(name string, help string) prometheus.CounterOpts { + return prometheus.CounterOpts{ + Namespace: NAMESPACE, + Name: name, + Help: help, + } +} + +// newGaugeOpts creates a GaugeOpts object, but with a predefined namespace +func newGaugeOpts(name string, help string) prometheus.GaugeOpts { + return prometheus.GaugeOpts{ + Namespace: NAMESPACE, + Name: name, + Help: help, + } +} diff --git a/prometheus/registration.go b/prometheus/registration.go new file mode 100644 index 00000000..3eb8c8a6 --- /dev/null +++ b/prometheus/registration.go @@ -0,0 +1,75 @@ +package prometheus + +import ( + "log" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/volkszaehler/mbmd/meters" +) + +var MBMDRegistry = prometheus.NewRegistry() + +type Config struct { + Enable bool // defaults to yes + EnableProcessCollector bool + EnableGoCollector bool +} + +// RegisterAllMetrics registers all static metrics and dynamically created measurement metrics +// to the Prometheus Default registry. +func RegisterAllMetrics(c Config) { + if !c.Enable { + return + } + createMeasurementMetrics() + if c.EnableProcessCollector { + MBMDRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + } + if c.EnableGoCollector { + MBMDRegistry.MustRegister(collectors.NewGoCollector()) + } +} + +// createMeasurementMetrics initializes all existing meters.Measurement +// +// If a prometheus.Metric could not be registered, the affected prometheus.Metric will be omitted. +func createMeasurementMetrics() { + for _, measurement := range meters.MeasurementValues() { + switch measurement.PrometheusMetricType() { + case meters.Gauge: + newGauge := NewMeasurementGaugeCollector( + newGaugeOpts( + measurement.PrometheusName(), + measurement.PrometheusHelpText(), + ), + ) + + if err := MBMDRegistry.Register(newGauge); err != nil { + log.Printf( + "Could not register gauge for measurement '%s'. Omitting... (Error: %s)\n", + measurement, + err, + ) + } else { + gaugeVecMap[measurement] = newGauge + } + case meters.Counter: + measurementCollector := NewMeasurementCounterCollector( + newCounterOpts( + measurement.PrometheusName(), + measurement.PrometheusHelpText(), + ), + ) + + if err := MBMDRegistry.Register(measurementCollector); err != nil { + log.Printf("could not register counter for measurement '%s'. omitting... (Error: %s)\n", + measurement, + err, + ) + } else { + counterVecMap[measurement] = measurementCollector + } + } + } +} diff --git a/server/handler.go b/server/handler.go index 6cd2bc7f..f770c5ad 100644 --- a/server/handler.go +++ b/server/handler.go @@ -9,6 +9,7 @@ import ( "time" "github.com/volkszaehler/mbmd/meters" + "github.com/volkszaehler/mbmd/prometheus" ) const ( @@ -75,7 +76,7 @@ func (h *Handler) Run( } if queryable, wakeup := status.IsQueryable(); wakeup { - log.Printf("device %s is offline - reactivating", deviceID) + log.Printf("device '%s' (%s) is offline - reactivating", dev.Descriptor().Name, deviceID) } else if !queryable { return } @@ -95,7 +96,7 @@ func (h *Handler) initializeDevice( if err := dev.Initialize(h.Manager.Conn.ModbusClient()); err != nil { if !errors.Is(err, meters.ErrPartiallyOpened) { - log.Printf("initializing device %s failed: %v", deviceID, err) + log.Printf("initializing device '%s' (%s) failed: %v", dev.Descriptor().Name, deviceID, err) // wait for error to settle ctx, cancel := context.WithTimeout(ctx, initDelay) @@ -107,7 +108,7 @@ func (h *Handler) initializeDevice( log.Println(err) // log error but continue } - log.Printf("initialized device %s: %v", deviceID, dev.Descriptor()) + log.Printf("initialized device '%s' (%s): %v", dev.Descriptor().Name, deviceID, dev.Descriptor()) // create status status := &RuntimeInfo{Online: true} @@ -129,12 +130,13 @@ func (h *Handler) queryDevice( dev meters.Device, ) { deviceID := h.deviceID(id, dev) + deviceDescriptor := dev.Descriptor() status := h.status[deviceID] for retry := 0; retry < maxRetry; retry++ { status.Requests++ - measurements, err := dev.Query(h.Manager.Conn.ModbusClient()) + measurements, err := dev.Query(h.Manager.Conn.ModbusClient()) if err == nil { // send ok status status.Available(true) @@ -146,7 +148,7 @@ func (h *Handler) queryDevice( // send measurements for _, r := range measurements { if math.IsNaN(r.Value) { - log.Printf("device %s skipping NaN for %s", deviceID, r.Measurement.String()) + log.Printf("device '%s' (%s) skipping NaN for %s", dev.Descriptor().Name, deviceID, r.Measurement.String()) continue } @@ -155,13 +157,15 @@ func (h *Handler) queryDevice( MeasurementResult: r, } results <- snip + + prometheus.UpdateMeasurementMetric(deviceDescriptor.Name, deviceDescriptor.Serial, r) } return } status.Errors++ - log.Printf("device %s did not respond (%d/%d): %v", deviceID, retry+1, maxRetry, err) + log.Printf("device '%s' (%s) did not respond (%d/%d): %v", dev.Descriptor().Name, deviceID, retry+1, maxRetry, err) // wait for device to settle after error select { @@ -171,7 +175,7 @@ func (h *Handler) queryDevice( } } - log.Printf("device %s is offline", deviceID) + log.Printf("device '%s' (%s) is offline", dev.Descriptor().Name, deviceID) // close connection to force modbus client to reopen h.Manager.Conn.Close() diff --git a/server/http.go b/server/http.go index 8299c3a3..1205cf0f 100644 --- a/server/http.go +++ b/server/http.go @@ -15,6 +15,8 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus/promhttp" + mbmd_prometheus "github.com/volkszaehler/mbmd/prometheus" ) // Assets is the embedded assets file system @@ -91,7 +93,7 @@ func (h *Httpd) allDevicesHandler( func (h *Httpd) singleDeviceHandler( readingsProvider func(id string) (*Readings, error), ) func(http.ResponseWriter, *http.Request) { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, ok := vars["id"] @@ -113,17 +115,17 @@ func (h *Httpd) singleDeviceHandler( if err := json.NewEncoder(w).Encode(data); err != nil { log.Printf("httpd: failed to encode JSON %s", err.Error()) } - }) + } } // mkSocketHandler attaches status handler to uri func (h *Httpd) mkStatusHandler(s *Status) func(http.ResponseWriter, *http.Request) { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(s); err != nil { log.Printf("httpd: failed to encode JSON: %s", err.Error()) } - }) + } } // mkSocketHandler attaches websocket handler to uri @@ -154,6 +156,16 @@ func jsonHandler(h http.Handler) http.Handler { }) } +// prometheusHandler is a middleware that prepares Prometheus metrics for collection by HTTP request +func (h *Httpd) prometheusHandler() http.Handler { + return promhttp.HandlerFor( + mbmd_prometheus.MBMDRegistry, + promhttp.HandlerOpts{ + EnableOpenMetrics: true, + }, + ) +} + // NewHttpd creates HTTP daemon func NewHttpd(hub *SocketHub, s *Status, qe DeviceInfo, mc *Cache) *Httpd { srv := &Httpd{ @@ -172,6 +184,10 @@ func NewHttpd(hub *SocketHub, s *Status, qe DeviceInfo, mc *Cache) *Httpd { static.PathPrefix("/" + dir).Handler(http.FileServer(http.FS(Assets))) } + // Prometheus + prom := srv.router.Path("/metrics") + prom.Handler(srv.prometheusHandler()) + // api api := srv.router.PathPrefix("/api").Subrouter() api.Use(jsonHandler) diff --git a/server/mqtt.go b/server/mqtt.go index b7cf4aee..7171710d 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -15,9 +15,7 @@ const ( publishTimeout = 2000 * time.Millisecond ) -var ( - topicRE = regexp.MustCompile(`(\w+)([LTS]\d)`) -) +var topicRE = regexp.MustCompile(`(\w+)([LTS]\d)`) // MqttClient is a MQTT publisher type MqttClient struct { diff --git a/server/status.go b/server/status.go index 9d2467fa..a6751fd4 100644 --- a/server/status.go +++ b/server/status.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "math" "runtime" "sync" "time" @@ -25,6 +26,8 @@ type ModbusStatus struct { type DeviceStatus struct { Device string Type string + Model string + Serial string Online bool ModbusStatus } @@ -41,13 +44,13 @@ func memoryStatus() MemoryStatus { // Status represents the daemon and device status. // It is updated when marshaled to JSON type Status struct { - sync.Mutex qe DeviceInfo StartTime time.Time UpTime float64 Goroutines int Memory MemoryStatus Meters []DeviceStatus + mu sync.RWMutex meterMap map[string]DeviceStatus } @@ -65,27 +68,26 @@ func NewStatus(qe DeviceInfo, control <-chan ControlSnip) *Status { go func() { for c := range control { - s.Lock() + s.mu.Lock() minutes := s.UpTime / 60 - mbs := ModbusStatus{ - Requests: c.Status.Requests, - Errors: c.Status.Errors, - ErrorsPerMinute: float64(c.Status.Errors) / minutes, - RequestsPerMinute: float64(c.Status.Requests) / minutes, - } desc := s.qe.DeviceDescriptorByID(c.Device) - - ds := DeviceStatus{ - Device: c.Device, - Type: desc.Manufacturer, - Online: c.Status.Online, - ModbusStatus: mbs, + s.meterMap[c.Device] = DeviceStatus{ + Device: c.Device, + Type: desc.Manufacturer, + Model: desc.Model, + Serial: desc.Serial, + Online: c.Status.Online, + ModbusStatus: ModbusStatus{ + Requests: c.Status.Requests, + Errors: c.Status.Errors, + ErrorsPerMinute: math.Round(float64(c.Status.Errors)/minutes*1000) / 1000, + RequestsPerMinute: math.Round(float64(c.Status.Requests)/minutes*1000) / 1000, + }, } - s.meterMap[c.Device] = ds - s.Unlock() + s.mu.Unlock() } }() @@ -94,8 +96,8 @@ func NewStatus(qe DeviceInfo, control <-chan ControlSnip) *Status { // Online returns device's online status or false if the device does not exist func (s *Status) Online(device string) bool { - s.Lock() - defer s.Unlock() + s.mu.RLock() + defer s.mu.RUnlock() if ds, ok := s.meterMap[device]; ok { return ds.Online @@ -110,7 +112,7 @@ func (s *Status) update() { s.Goroutines = runtime.NumGoroutine() s.UpTime = time.Since(s.StartTime).Seconds() - s.Meters = make([]DeviceStatus, 0) + s.Meters = make([]DeviceStatus, 0, len(s.meterMap)) for _, ms := range s.meterMap { s.Meters = append(s.Meters, ms) } @@ -119,8 +121,8 @@ func (s *Status) update() { // MarshalJSON will syncronize access to the status object // see http://choly.ca/post/go-json-marshalling/ for avoiding infinite loop func (s *Status) MarshalJSON() ([]byte, error) { - s.Lock() - defer s.Unlock() + s.mu.Lock() + defer s.mu.Unlock() s.update()