Compare commits

...

36 Commits

Author SHA1 Message Date
ce0ef7d291 Clear invalid measurements
All checks were successful
Build and Publish / release (push) Successful in 3m34s
2025-01-28 19:26:40 -05:00
f2f160b112 Fix wunderground wind speed mapping
All checks were successful
Build and Publish / release (push) Successful in 2m40s
2025-01-28 17:05:25 -05:00
2c29493229 Upgrade, fix attrs
All checks were successful
Build and Publish / release (push) Successful in 3m46s
2025-01-28 15:47:58 -05:00
59433cc77f Upgrades
All checks were successful
Build and Publish / release (push) Successful in 4m27s
2025-01-28 15:08:31 -05:00
16d3f9cb17 Add grafana dashboard 2025-01-12 21:44:34 -05:00
e93c7ec5c0 Update go-app
All checks were successful
Build and Publish / release (push) Successful in 6m10s
2025-01-12 20:22:56 -05:00
c2bde1e1dd Update TODO 2025-01-12 19:27:10 -05:00
44bf293eab Proxy observability updates
All checks were successful
Build and Publish / release (push) Successful in 2m51s
2025-01-12 19:14:24 -05:00
f87b35b306 Perform proxy updates in goroutines 2025-01-12 19:10:30 -05:00
eaf2cf211d Update wunderground url
All checks were successful
Build and Publish / release (push) Successful in 2m36s
2025-01-12 19:06:45 -05:00
e8fbacfe6d Proxy updates for AWN
All checks were successful
Build and Publish / release (push) Successful in 2m53s
2025-01-12 17:49:54 -05:00
19823ea08f Weather service proxy support
All checks were successful
Build and Publish / release (push) Successful in 3m51s
2025-01-12 17:23:32 -05:00
7fc1fc9b56 Proxy support for AWN/Wunderground 2025-01-12 15:30:37 -05:00
849dbfb6ff Update go-app
All checks were successful
Build and Publish / release (push) Successful in 4m5s
2025-01-08 21:31:50 -05:00
dc2470da71 Improve metrics recording
Some checks failed
Build and Publish / release (push) Failing after 40s
2025-01-08 21:28:38 -05:00
c23177b62d Support metric prefix change
Some checks failed
Build and Publish / release (push) Failing after 40s
2025-01-08 16:59:27 -05:00
86653cf589 Use custom config type 2025-01-08 16:49:31 -05:00
fa0a9f4ddc Fix wind speed mapping for awn
All checks were successful
Build and Publish / release (push) Successful in 2m46s
2025-01-08 14:24:56 -05:00
689790fe86 Update TODO 2025-01-08 14:24:46 -05:00
3af1cc40a5 Fix metric prefix
All checks were successful
Build and Publish / release (push) Successful in 3m29s
2025-01-07 15:08:14 -05:00
4ff684abe3 Update TODO 2025-01-07 11:35:14 -05:00
87f0cbac01 Fix typos, update TODO
All checks were successful
Build and Publish / release (push) Successful in 3m44s
2025-01-07 11:28:34 -05:00
4c93303f27 Consolidate battery metrics 2025-01-07 10:27:02 -05:00
e8654e76bc Update TODO 2025-01-07 08:53:06 -05:00
54f725c822 Update go-app 2025-01-05 20:33:28 -05:00
c002331fd4 Fix NaN with zero values
All checks were successful
Build and Publish / release (push) Successful in 3m7s
2025-01-05 19:05:24 -05:00
2ed4eca56c Add support for co2 battery
All checks were successful
Build and Publish / release (push) Successful in 2m57s
2025-01-05 18:39:58 -05:00
2e3cfb44f4 Don't enrich nil updates
All checks were successful
Build and Publish / release (push) Successful in 2m48s
2025-01-05 17:37:14 -05:00
be8a4f3bca Don't enrich nil updates 2025-01-05 17:36:39 -05:00
96dfcf7e53 Update sample
All checks were successful
Build and Publish / release (push) Successful in 3m58s
2025-01-05 16:24:52 -05:00
5fc16b39af Upgrade go-app 2025-01-05 16:23:15 -05:00
1ae58f64ca Move docker compose sample 2025-01-05 15:16:16 -05:00
bb4e4aa2af Update TODO 2025-01-05 13:19:30 -05:00
f102ef50bf Upgrade to fix env overrides
All checks were successful
Build and Publish / release (push) Successful in 4m7s
2025-01-05 13:13:19 -05:00
d3d251ef30 Update TODO 2025-01-05 11:47:06 -05:00
eecaf2a82e Add latest tag
All checks were successful
Build and Publish / release (push) Successful in 2m44s
2025-01-05 11:44:48 -05:00
22 changed files with 5970 additions and 293 deletions

View File

@ -79,7 +79,9 @@ jobs:
with: with:
context: . context: .
push: true push: true
tags: ${{ env.DOCKER_IMG }}:${{ github.ref_name }} tags: |
${{ env.DOCKER_IMG }}:${{ github.ref_name }}
${{ env.DOCKER_IMG }}:latest
build-args: | build-args: |
VER_PKG=${{ env.VER_PKG }} VER_PKG=${{ env.VER_PKG }}
VERSION=${{ github.ref_name }} VERSION=${{ github.ref_name }}

4
.gitignore vendored
View File

@ -20,7 +20,11 @@
# Go workspace file # Go workspace file
go.work go.work
go.work.sum
.env .env
bin/* bin/*
config.yaml
docker-compose.yml

26
TODO.md
View File

@ -1,10 +1,22 @@
# TODO # TODO
- [ ] Fix shutdown
- [ ] Configuration for app
- [ ] Makefile
- [ ] Dockerfile
- [ ] Helm Chart - [ ] Helm Chart
- [ ] Gitea CI
- [ ] Update README - [ ] Update README
- [ ] Version flag - [ ] Add Grafana dashboard
- [ ] Add new spans
## Done
- [x] Add proxy to upstream support
- [x] Fix wunderground 401
- [x] Perform proxy calls in goroutines
- [x] Configuration for app
- [x] Configurable metric prefix
- [x] Add device name field with ID/Key mappings
- [x] Add device type field with ID/Key mappings
- [x] Move EVERYTHING to pointers to support nil
- [x] Consolidate battery status into one metric with device label
- [x] Fix shutdown
- [x] Add new fields from WS-2192
- [x] Gitea CI
- [x] Version flag
- [x] Makefile
- [x] Dockerfile

View File

@ -10,6 +10,10 @@ services:
APP_LOG_FORMAT: json ## console, json APP_LOG_FORMAT: json ## console, json
APP_LOG_TIME_FORMAT: rfc3339 ## long, short, unix, rfc3339, off APP_LOG_TIME_FORMAT: rfc3339 ## long, short, unix, rfc3339, off
APP_HTTP_LISTEN: 0.0.0.0:8080 APP_HTTP_LISTEN: 0.0.0.0:8080
APP_HTTP_READ_TIMEOUT: 10s
APP_HTTP_WRITE_TIMEOUT: 10s
APP_HTTP_IDLE_TIMEOUT: 30s
APP_HTTP_LOG_REQUESTS: true
APP_OTEL_STDOUT_ENABLED: false APP_OTEL_STDOUT_ENABLED: false
APP_OTEL_METRIC_INTERVAL_SECS: 30 APP_OTEL_METRIC_INTERVAL_SECS: 30
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel.libretechconsulting.com:4317 # Set to your otel collector OTEL_EXPORTER_OTLP_ENDPOINT: http://otel.libretechconsulting.com:4317 # Set to your otel collector

File diff suppressed because it is too large Load Diff

8
contrib/unset_env.sh Executable file
View File

@ -0,0 +1,8 @@
#!env sh
unset APP_NAME
unset APP_LOG_LEVEL
unset APP_LOG_FORMAT
unset APP_LOG_TIME_FORMAT
unset APP_HTTP_LISTEN
unset APP_OTEL_STDOUT_ENABLED
unset APP_OTEL_METRIC_INTERVAL_SECS

48
go.mod
View File

@ -3,49 +3,51 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter
go 1.23.4 go 1.23.4
require ( require (
gitea.libretechconsulting.com/rmcguire/go-app v0.1.1 gitea.libretechconsulting.com/rmcguire/go-app v0.4.2
github.com/go-resty/resty/v2 v2.16.5
github.com/gorilla/schema v1.4.1 github.com/gorilla/schema v1.4.1
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
go.opentelemetry.io/otel v1.33.0 go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/metric v1.33.0 go.opentelemetry.io/otel/metric v1.34.0
golang.org/x/sys v0.29.0 golang.org/x/sys v0.29.0
k8s.io/utils v0.0.0-20241210054802-24370beab758
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/caarlos0/env/v9 v9.0.0 // indirect github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.56.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
google.golang.org/grpc v1.69.2 // indirect google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

103
go.sum
View File

@ -1,11 +1,13 @@
gitea.libretechconsulting.com/rmcguire/go-app v0.1.0 h1:H4TMgQ463oRNOyoi0FAvfGtOoDn651zNZStxM+sdNuU= gitea.libretechconsulting.com/rmcguire/go-app v0.4.0 h1:iEGuA2D15rniiKlgejykxvs0TBD9JigEVnhYiCNppw4=
gitea.libretechconsulting.com/rmcguire/go-app v0.1.0/go.mod h1:p0ajkpFvzzD6VZ4xSjuowtwGRb1DjMfo/iG6LyFqFCs= gitea.libretechconsulting.com/rmcguire/go-app v0.4.0/go.mod h1:ug6g+FyEi2LguWTQfd+bZrTd1ECsot8BylxgMFEO5DM=
gitea.libretechconsulting.com/rmcguire/go-app v0.1.1 h1:Hrxqi1tqz8mf0baBsWgFe/S4jyMtIuPqH2FlanJUMNc= gitea.libretechconsulting.com/rmcguire/go-app v0.4.1 h1:gjDg2M/j1AdMCtkXqQnLCo6jJUSWQOj56ehfU1S6BGE=
gitea.libretechconsulting.com/rmcguire/go-app v0.1.1/go.mod h1:p0ajkpFvzzD6VZ4xSjuowtwGRb1DjMfo/iG6LyFqFCs= gitea.libretechconsulting.com/rmcguire/go-app v0.4.1/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs=
gitea.libretechconsulting.com/rmcguire/go-app v0.4.2 h1:LQxVLXEHruY32GaMsS5K/tMdjS5kvw6reUh25gshn40=
gitea.libretechconsulting.com/rmcguire/go-app v0.4.2/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@ -20,6 +22,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
@ -29,8 +33,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 h1:VD1gqscl4nYs1YxVuSdemTrSgTKrwOWDK0FVFMqm+Cg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0/go.mod h1:4EgsQoS4TOhJizV+JTFg40qx1Ofh3XmXEQNBpgvNT40=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -39,8 +43,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -54,8 +59,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 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 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 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/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@ -67,36 +72,36 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo= go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E=
go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI= go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 h1:czJDQwFrMbOr9Kk+BPo1y8WZIIFIK58SA1kykuVeiOU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0/go.mod h1:lT7bmsxOe58Tq+JIOkTQMCGXdu47oA+VJKLZHbaBKbs=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -104,16 +109,20 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287 h1:A2ni10G3UlplFrWdCDJTl7D7mJ7GSRm37S+PDimaKRw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/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 v0.0.0-20161208181325-20d25e280405/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=

20
main.go
View File

@ -10,17 +10,29 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
) )
const defaultMetricPrefix = "weather"
func main() { func main() {
ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, unix.SIGTERM) ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, unix.SIGTERM)
defer cncl() defer cncl()
ctx = app.MustSetupConfigAndLogging(ctx) // Config type for app, which implements go-app/config.AppConfig
awConfig := &config.AmbientLocalExporterConfig{
MetricPrefix: defaultMetricPrefix,
WeatherStations: make([]config.WeatherStation, 0),
}
aw := ambient.New(ctx) // Read config and environment, set up logging, load up
aw.MustInit() // an appCtx, and prepare ambient weather local exporter
ctx, awConfig = app.MustLoadConfigInto(ctx, awConfig)
// Prepare the exporter
aw := ambient.New(ctx, awConfig).Init()
// Define and prepare the app
awApp := app.App{ awApp := app.App{
AppContext: ctx, AppContext: ctx,
HTTP: &app.AppHTTP{ HTTP: &app.AppHTTP{
@ -43,7 +55,7 @@ func main() {
}, },
} }
// Run and wait
awApp.MustRun() awApp.MustRun()
<-awApp.Done() <-awApp.Done()
} }

View File

@ -7,12 +7,14 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"sync"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/awn" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/awn"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/wunderground" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/wunderground"
@ -24,6 +26,7 @@ type AmbientWeather struct {
// when either "AmbientWeather" or "Wunderground" are selected // when either "AmbientWeather" or "Wunderground" are selected
// in the "Custom" section of the AWNet app, or the web UI // in the "Custom" section of the AWNet app, or the web UI
// of an Ambient WeatherHub // of an Ambient WeatherHub
config *config.AmbientLocalExporterConfig
awnProvider provider.AmbientProvider awnProvider provider.AmbientProvider
wuProvider provider.AmbientProvider wuProvider provider.AmbientProvider
appCtx context.Context appCtx context.Context
@ -31,16 +34,21 @@ type AmbientWeather struct {
l *zerolog.Logger l *zerolog.Logger
} }
func New(appCtx context.Context) *AmbientWeather { func New(appCtx context.Context, awConfig *config.AmbientLocalExporterConfig) *AmbientWeather {
return &AmbientWeather{ return &AmbientWeather{
config: awConfig,
appCtx: appCtx, appCtx: appCtx,
} }
} }
func (aw *AmbientWeather) MustInit() { // Initialize with defaults, set logger from context
func (aw *AmbientWeather) Init() *AmbientWeather {
aw.awnProvider = &awn.AWNProvider{} aw.awnProvider = &awn.AWNProvider{}
aw.wuProvider = &wunderground.WUProvider{} aw.wuProvider = &wunderground.WUProvider{}
aw.l = zerolog.Ctx(aw.appCtx) aw.l = zerolog.Ctx(aw.appCtx)
aw.l.Trace().Any("awConfig", aw.config).Send()
return aw
} }
func (aw *AmbientWeather) GetAWNHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) { func (aw *AmbientWeather) GetAWNHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) {
@ -55,7 +63,7 @@ func (aw *AmbientWeather) GetWundergroundHandlerFunc(appCtx context.Context) fun
} }
} }
// Takes an HTTP requests and convers it to a // Takes an HTTP requests and converts it to a
// stable type. Enrich is called on the type to complete // stable type. Enrich is called on the type to complete
// any missing fields as the two providers supported by Ambient // any missing fields as the two providers supported by Ambient
// devices (awn/wunderground) produce different fields // devices (awn/wunderground) produce different fields
@ -85,16 +93,22 @@ func (aw *AmbientWeather) handleProviderRequest(
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))
return
} }
// Calculate any fields that may be missing // Calculate any fields that may be missing
// such as dew point and wind chill // such as dew point and wind chill
update.Enrich() update.Enrich()
// Update metrics // Prepare metrics if this is the first update
if aw.metrics == nil { if aw.metrics == nil {
aw.metrics = weather.MustInitMetrics(aw.appCtx) aw.InitMetrics()
} }
// Enrich station if configured
aw.enrichStation(update)
// Update metrics
aw.metrics.Update(update) aw.metrics.Update(update)
l.Debug(). l.Debug().
@ -102,4 +116,61 @@ func (aw *AmbientWeather) handleProviderRequest(
Any("update", update). Any("update", update).
Msg("successfully handled update") Msg("successfully handled update")
w.Write([]byte("ok")) w.Write([]byte("ok"))
// Proxy update to one or both services if configured to do so
// Uses a weather update to allow awn to publish to wunderground and
// visa versa.
if station := update.StationConfig; station != nil {
// Perform proxy updates in parallel if enabled
var proxyWg sync.WaitGroup
if station.ProxyToAWN {
proxyWg.Add(1)
go func() {
defer proxyWg.Done()
err := aw.awnProvider.ProxyReq(ctx, update)
if err != nil {
zerolog.Ctx(aw.appCtx).Err(err).Msg("failed to proxy to ambient weather")
return
}
zerolog.Ctx(aw.appCtx).Debug().
Str("station", station.Name).
Msg("proxied weather update to awn")
}()
}
if station.ProxyToWunderground {
proxyWg.Add(1)
go func() {
defer proxyWg.Done()
err := aw.wuProvider.ProxyReq(ctx, update)
if err != nil {
zerolog.Ctx(aw.appCtx).Err(err).Msg("failed to proxy to ambient weather")
return
}
zerolog.Ctx(aw.appCtx).Debug().
Str("station", station.Name).
Msg("proxied weather update to wunderground")
}()
}
proxyWg.Wait()
}
}
func (aw *AmbientWeather) InitMetrics() {
if aw.config.MetricPrefix != "" {
weather.MetricPrefix = aw.config.MetricPrefix
}
aw.metrics = weather.MustInitMetrics(aw.appCtx)
}
func (aw *AmbientWeather) enrichStation(update *weather.WeatherUpdate) {
if update != nil && update.StationID != nil && *update.StationID != "" {
for _, station := range aw.config.WeatherStations {
if *update.StationID == station.AWNPassKey || *update.StationID == station.WundergroundID {
update.StationConfig = &station
}
}
}
} }

View File

@ -0,0 +1,36 @@
package config
import (
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
)
// This configuration includes all config from go-app/config.AppConfig
type AmbientLocalExporterConfig struct {
MetricPrefix string `yaml:"metricPrefix" default:"weather" env:"AMBIENT_METRIC_PREFIX"`
WeatherStations []WeatherStation `yaml:"weatherStations"` // No env, too complex, not worth the time
*config.AppConfig // Extends app config
}
type WeatherStation struct {
Name string `yaml:"name"` // Human Friendly Name (e.g. Back Yard Weather)
Equipment string `yaml:"equipment"` // Equipment Type (e.g. WS-5000)
// Required if proxying to awn/wu is enabled
WundergroundID string `yaml:"wundergroundID"`
WundergroundPassword string `yaml:"wundergroundPassword"`
AWNPassKey string `yaml:"awnPassKey"`
// Proxy updates to AWN or Wunderground
ProxyToAWN bool `yaml:"proxyToAWN"`
ProxyToWunderground bool `yaml:"proxyToWunderground"`
// Unreliable / unwanted metrics by name of WeatherUpdate Field
// will be excluded if present in discardMetrics
//
// If anything is present in keepMetrics, it is solely applied,
// ignoring discardMetrics.
//
// Check weather.WeatherUpdateField for options
KeepMetrics []string `yaml:"keepMetrics"`
DropMetrics []string `yaml:"dropMetrics"`
}

View File

@ -13,7 +13,18 @@ import (
type AWNProvider struct{} type AWNProvider struct{}
const providerName = "awn" const (
providerName = "awn"
awnURL = "http://ambientweather.net/data/report"
)
// Battery Sensors
const (
BattOutdoorSensor = "OutdoorSensor"
BattIndoorSensor = "IndoorSensor"
BattRainSensor = "RainSensor"
BattCO2Sensor = "CO2Sensor"
)
func (awn *AWNProvider) Name() string { func (awn *AWNProvider) Name() string {
return providerName return providerName
@ -33,37 +44,56 @@ func (awn *AWNProvider) ReqToWeather(_ context.Context, r *http.Request) (
} }
func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate {
updateTime, err := time.Parse(time.DateTime, awnUpdate.DateUTC) updateTime := time.Now()
if err != nil { if awnUpdate.DateUTC != nil {
updateTime = time.Now() ut, err := time.Parse(time.DateTime, *awnUpdate.DateUTC)
if err == nil {
updateTime = ut
}
} }
return &weather.WeatherUpdate{ return &weather.WeatherUpdate{
StationType: awnUpdate.StationType, DateUTC: &updateTime,
DateUTC: &updateTime, StationID: awnUpdate.PassKey,
TempOutdoorF: awnUpdate.TempF, StationType: awnUpdate.StationType,
HumidityOudoor: awnUpdate.Humidity, TempOutdoorF: awnUpdate.TempF,
WindSpeedMPH: awnUpdate.WindGustMPH, HumidityOudoor: awnUpdate.Humidity,
WindGustMPH: awnUpdate.WindGustMPH, WindSpeedMPH: awnUpdate.WindSpeedMPH,
MaxDailyGust: awnUpdate.MaxDailyGust, WindGustMPH: awnUpdate.WindGustMPH,
WindDir: awnUpdate.WindDir, MaxDailyGust: awnUpdate.MaxDailyGust,
WindDirAvg10m: awnUpdate.WindDirAVG10m, WindDir: awnUpdate.WindDir,
UV: awnUpdate.UV, WindDirAvg10m: awnUpdate.WindDirAVG10m,
SolarRadiation: awnUpdate.SolarRadiation, UV: awnUpdate.UV,
HourlyRainIn: awnUpdate.HourlyRainIn, SolarRadiation: awnUpdate.SolarRadiation,
EventRainIn: awnUpdate.EventRainIn, HourlyRainIn: awnUpdate.HourlyRainIn,
DailyRainIn: awnUpdate.DailyRainIn, EventRainIn: awnUpdate.EventRainIn,
WeeklyRainIn: awnUpdate.WeeklyRainIn, DailyRainIn: awnUpdate.DailyRainIn,
MonthlyRainIn: awnUpdate.MonthlyRainIn, WeeklyRainIn: awnUpdate.WeeklyRainIn,
YearlyRainIn: awnUpdate.YearlyRainIn, MonthlyRainIn: awnUpdate.MonthlyRainIn,
TotalRainIn: awnUpdate.TotalRainIn, YearlyRainIn: awnUpdate.YearlyRainIn,
BattOutdoorSensor: awnUpdate.BattOut, TotalRainIn: awnUpdate.TotalRainIn,
BattIndoorSensor: awnUpdate.BattIn, Batteries: []weather.BatteryStatus{
BattRainSensor: awnUpdate.BattRain, {
TempIndoorF: awnUpdate.TempInF, Component: BattOutdoorSensor,
HumidityIndoor: awnUpdate.HumidityIn, Status: awnUpdate.BattOut,
BaromRelativeIn: awnUpdate.BaromRelIn, },
BaromAbsoluteIn: awnUpdate.BaromAbsIn, {
Component: BattIndoorSensor,
Status: awnUpdate.BattIn,
},
{
Component: BattRainSensor,
Status: awnUpdate.BattRain,
},
{
Component: BattCO2Sensor,
Status: awnUpdate.BattCO2,
},
},
TempIndoorF: awnUpdate.TempInF,
HumidityIndoor: awnUpdate.HumidityIn,
BaromRelativeIn: awnUpdate.BaromRelIn,
BaromAbsoluteIn: awnUpdate.BaromAbsIn,
} }
} }

105
pkg/provider/awn/proxy.go Normal file
View File

@ -0,0 +1,105 @@
package awn
import (
"context"
"errors"
"net/url"
"time"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/weather"
)
// Attempts to proxy the weather station update to awn
// SAMPLE:
// {"PASSKEY":["ABF7E052BC7325A32300ACC89112AA91"],"baromabsin":["28.895"],
// "baromrelin":["29.876"],"battin":["1"],"battout":["1"],"battrain":["1"],
// "dailyrainin":["0.000"],"dateutc":["2025-01-11 22:07:57"],"eventrainin":["0.000"],
// "hourlyrainin":["0.000"],"humidity":["76"],"humidityin":["31"],"maxdailygust":["7.83"],
// "monthlyrainin":["0.000"],"solarradiation":["14.21"],"stationtype":["WeatherHub_V1.0.1"],
// "tempf":["29.48"],"tempinf":["66.20"],"totalrainin":["0.000"],"uv":["0"],
// "weeklyrainin":["0.000"],"winddir":["66"],"winddir_avg10m":["268"],"windgustmph":["2.68"],
// "windspeedmph":["0.00"],"yearlyrainin":["0.000"]}
func (awn *AWNProvider) ProxyReq(ctx context.Context, update *weather.WeatherUpdate) error {
tracer := otel.GetTracer(ctx, "awnProvider", "proxyReq")
ctx, span := tracer.Start(ctx, "proxyToAWN")
defer span.End()
if update.StationConfig.AWNPassKey == "" {
err := errors.New("no PASSKEY set in update")
span.RecordError(err)
return err
}
params := updateToAWNParams(update)
resp, err := resty.New().R().
SetContext(ctx).
SetQueryParamsFromValues(*params).
Get(awnURL)
if err != nil {
span.RecordError(err)
span.SetAttributes(
attribute.String("query", resp.Request.QueryParam.Encode()),
attribute.String("body", string(resp.Body())),
)
span.SetStatus(codes.Error, err.Error())
log.Err(err).Any("query", resp.Request.PathParams).
Int("statusCode", resp.StatusCode()).
Msg("awn proxy failed")
}
span.SetAttributes(
attribute.Int("statusCode", resp.StatusCode()),
)
return err
}
func updateToAWNParams(update *weather.WeatherUpdate) *url.Values {
params := &url.Values{}
params.Set("dateutc", time.Now().Format(time.DateTime))
weather.SetURLVal(params, "PASSKEY", &update.StationConfig.AWNPassKey)
weather.SetURLVal(params, "baromabsin", update.BaromAbsoluteIn)
weather.SetURLVal(params, "baromrelin", update.BaromRelativeIn)
weather.SetURLVal(params, "dailyrainin", update.DailyRainIn)
weather.SetURLVal(params, "weeklyrainin", update.WeeklyRainIn)
weather.SetURLVal(params, "eventrainin", update.EventRainIn)
weather.SetURLVal(params, "hourlyrainin", update.HourlyRainIn)
weather.SetURLVal(params, "monthlyrainin", update.MonthlyRainIn)
weather.SetURLVal(params, "yearlyrainin", update.YearlyRainIn)
weather.SetURLVal(params, "totalrainin", update.TotalRainIn)
weather.SetURLVal(params, "humidity", update.HumidityOudoor)
weather.SetURLVal(params, "humidityin", update.HumidityIndoor)
weather.SetURLVal(params, "solarradiation", update.SolarRadiation)
weather.SetURLVal(params, "uv", update.UV)
weather.SetURLVal(params, "stationtype", update.StationType)
weather.SetURLVal(params, "tempf", update.TempOutdoorF)
weather.SetURLVal(params, "tempinf", update.TempIndoorF)
weather.SetURLVal(params, "winddir", update.WindDir)
weather.SetURLVal(params, "winddir_avg10m", update.WindDirAvg10m)
weather.SetURLVal(params, "windgustmph", update.WindGustMPH)
weather.SetURLVal(params, "windspeedmph", update.WindSpeedMPH)
weather.SetURLVal(params, "maxdailygust", update.MaxDailyGust)
// Batteries
for _, status := range update.Batteries {
switch status.Component {
case BattOutdoorSensor:
weather.SetURLVal(params, "battin", status.Status)
case BattIndoorSensor:
weather.SetURLVal(params, "battout", status.Status)
case BattRainSensor:
weather.SetURLVal(params, "battrain", status.Status)
case BattCO2Sensor:
weather.SetURLVal(params, "batt_co2", status.Status)
}
}
return params
}

View File

@ -1,30 +1,31 @@
package awn package awn
type AmbientWeatherUpdate struct { type AmbientWeatherUpdate struct {
PassKey string `json:"PASSKEY,omitempty" schema:"PASSKEY"` PassKey *string `json:"PASSKEY,omitempty" schema:"PASSKEY"`
StationType string `json:"stationtype,omitempty" schema:"stationtype"` StationType *string `json:"stationtype,omitempty" schema:"stationtype"`
DateUTC string `json:"dateutc,omitempty" schema:"dateutc"` DateUTC *string `json:"dateutc,omitempty" schema:"dateutc"`
TempF float64 `json:"tempf,omitempty" schema:"tempf"` TempF *float64 `json:"tempf,omitempty" schema:"tempf"`
Humidity int `json:"humidity,omitempty" schema:"humidity"` Humidity *int `json:"humidity,omitempty" schema:"humidity"`
WindSpeedMPH float64 `json:"windspeedmph,omitempty" schema:"windspeedmph"` WindSpeedMPH *float64 `json:"windspeedmph,omitempty" schema:"windspeedmph"`
WindGustMPH float64 `json:"windgustmph,omitempty" schema:"windgustmph"` WindGustMPH *float64 `json:"windgustmph,omitempty" schema:"windgustmph"`
MaxDailyGust float64 `json:"maxdailygust,omitempty" schema:"maxdailygust"` MaxDailyGust *float64 `json:"maxdailygust,omitempty" schema:"maxdailygust"`
WindDir int `json:"winddir,omitempty" schema:"winddir"` WindDir *int `json:"winddir,omitempty" schema:"winddir"`
WindDirAVG10m int `json:"winddir_avg10m,omitempty" schema:"winddir_avg10m"` WindDirAVG10m *int `json:"winddir_avg10m,omitempty" schema:"winddir_avg10m"`
UV int `json:"uv,omitempty" schema:"uv"` UV *int `json:"uv,omitempty" schema:"uv"`
SolarRadiation float64 `json:"solarradiation,omitempty" schema:"solarradiation"` SolarRadiation *float64 `json:"solarradiation,omitempty" schema:"solarradiation"`
HourlyRainIn float64 `json:"hourlyrainin,omitempty" schema:"hourlyrainin"` HourlyRainIn *float64 `json:"hourlyrainin,omitempty" schema:"hourlyrainin"`
EventRainIn float64 `json:"eventrainin,omitempty" schema:"eventrainin"` EventRainIn *float64 `json:"eventrainin,omitempty" schema:"eventrainin"`
DailyRainIn float64 `json:"dailyrainin,omitempty" schema:"dailyrainin"` DailyRainIn *float64 `json:"dailyrainin,omitempty" schema:"dailyrainin"`
WeeklyRainIn float64 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"` WeeklyRainIn *float64 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"`
MonthlyRainIn float64 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"` MonthlyRainIn *float64 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"`
YearlyRainIn float64 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"` YearlyRainIn *float64 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"`
TotalRainIn float64 `json:"totalrainin,omitempty" schema:"totalrainin"` TotalRainIn *float64 `json:"totalrainin,omitempty" schema:"totalrainin"`
BattOut int `json:"battout,omitempty" schema:"battout"` BattOut *int `json:"battout,omitempty" schema:"battout"`
BattRain int `json:"battrain,omitempty" schema:"battrain"` BattRain *int `json:"battrain,omitempty" schema:"battrain"`
TempInF float64 `json:"tempinf,omitempty" schema:"tempinf"` TempInF *float64 `json:"tempinf,omitempty" schema:"tempinf"`
HumidityIn int `json:"humidityin,omitempty" schema:"humidityin"` HumidityIn *int `json:"humidityin,omitempty" schema:"humidityin"`
BaromRelIn float64 `json:"baromrelin,omitempty" schema:"baromrelin"` BaromRelIn *float64 `json:"baromrelin,omitempty" schema:"baromrelin"`
BaromAbsIn float64 `json:"baromabsin,omitempty" schema:"baromabsin"` BaromAbsIn *float64 `json:"baromabsin,omitempty" schema:"baromabsin"`
BattIn int `json:"battin,omitempty" schema:"battin"` BattIn *int `json:"battin,omitempty" schema:"battin"`
BattCO2 *int `json:"batt_co2,omitempty" schema:"batt_co2"`
} }

View File

@ -11,5 +11,6 @@ import (
// Ambient Weather Network HTTP requests to a stable struct // Ambient Weather Network HTTP requests to a stable struct
type AmbientProvider interface { type AmbientProvider interface {
ReqToWeather(context.Context, *http.Request) (*weather.WeatherUpdate, error) ReqToWeather(context.Context, *http.Request) (*weather.WeatherUpdate, error)
ProxyReq(context.Context, *weather.WeatherUpdate) error
Name() string Name() string
} }

View File

@ -13,7 +13,10 @@ import (
type WUProvider struct{} type WUProvider struct{}
const providerName = "weatherunderground" const (
providerName = "weatherunderground"
wuURL = "https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php"
)
func (wu *WUProvider) Name() string { func (wu *WUProvider) Name() string {
return providerName return providerName
@ -33,17 +36,22 @@ func (wu *WUProvider) ReqToWeather(_ context.Context, r *http.Request) (
} }
func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate { func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate {
updateTime, err := time.Parse(time.DateTime, wuUpdate.DateUTC) updateTime := time.Now()
if err != nil {
updateTime = time.Now() if wuUpdate.DateUTC != nil {
ut, err := time.Parse(time.DateTime, *wuUpdate.DateUTC)
if err == nil {
updateTime = ut
}
} }
return &weather.WeatherUpdate{ return &weather.WeatherUpdate{
StationType: wuUpdate.SoftwareType,
DateUTC: &updateTime, DateUTC: &updateTime,
TempOutdoorF: wuUpdate.Tempf, StationID: wuUpdate.ID,
HumidityOudoor: wuUpdate.Humidity, StationType: wuUpdate.SoftwareType,
WindSpeedMPH: wuUpdate.WindGustMPH, TempOutdoorF: wuUpdate.Tempf,
HumidityOudoor: wuUpdate.Humidity,
WindSpeedMPH: wuUpdate.WindSpeedMPH,
WindGustMPH: wuUpdate.WindGustMPH, WindGustMPH: wuUpdate.WindGustMPH,
WindDir: wuUpdate.WindDir, WindDir: wuUpdate.WindDir,
UV: wuUpdate.UV, UV: wuUpdate.UV,

View File

@ -0,0 +1,88 @@
package wunderground
import (
"context"
"errors"
"net/url"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"k8s.io/utils/ptr"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/weather"
)
func (wu *WUProvider) ProxyReq(ctx context.Context, update *weather.WeatherUpdate) error {
tracer := otel.GetTracer(ctx, "wuProvider", "proxyReq")
ctx, span := tracer.Start(ctx, "proxyToWunderground")
defer span.End()
if update.StationConfig.WundergroundID == "" {
err := errors.New("no wunderground id set in update")
span.RecordError(err)
return err
} else if update.StationConfig.WundergroundPassword == "" {
err := errors.New("no wunderground id set in update")
span.RecordError(err)
return err
}
params := updateToWuParams(update)
resp, err := resty.New().R().
SetContext(ctx).
SetQueryParamsFromValues(*params).
Get(wuURL)
if err != nil {
span.SetStatus(codes.Error, err.Error())
span.SetAttributes(
attribute.String("query", resp.Request.QueryParam.Encode()),
attribute.String("body", string(resp.Body())),
)
span.RecordError(err)
log.Err(err).
Int("statusCode", resp.StatusCode()).
Any("query", resp.Request.PathParams).
Msg("wunderground proxy failed")
}
span.SetAttributes(
attribute.Int("statusCode", resp.StatusCode()),
)
return err
}
func updateToWuParams(u *weather.WeatherUpdate) *url.Values {
params := &url.Values{}
weather.SetURLVal(params, "ID", &u.StationConfig.WundergroundID)
weather.SetURLVal(params, "PASSWORD", &u.StationConfig.WundergroundPassword)
params.Set("action", "updateraw")
params.Set("dateutc", "now")
weather.SetURLVal(params, "UV", u.UV)
weather.SetURLVal(params, "baromin", u.BaromRelativeIn)
weather.SetURLVal(params, "dailyrainin", u.DailyRainIn)
weather.SetURLVal(params, "dewptf", u.DewPointF)
weather.SetURLVal(params, "humidity", u.HumidityOudoor)
weather.SetURLVal(params, "indoorhumidity", u.HumidityIndoor)
weather.SetURLVal(params, "indoortempf", u.TempIndoorF)
weather.SetURLVal(params, "lowbatt", ptr.To(0))
weather.SetURLVal(params, "monthlyrainin", u.MonthlyRainIn)
weather.SetURLVal(params, "rainin", u.HourlyRainIn)
weather.SetURLVal(params, "realtime", ptr.To(1))
weather.SetURLVal(params, "rtfreq", ptr.To(60))
weather.SetURLVal(params, "softwaretype", u.StationType)
weather.SetURLVal(params, "solarradiation", u.SolarRadiation)
weather.SetURLVal(params, "tempf", u.TempOutdoorF)
weather.SetURLVal(params, "weeklyrainin", u.WeeklyRainIn)
weather.SetURLVal(params, "windchillf", u.WindChillF)
weather.SetURLVal(params, "winddir", u.WindDir)
weather.SetURLVal(params, "windgustmph", u.WindGustMPH)
weather.SetURLVal(params, "windspeedmph", u.WindSpeedMPH)
weather.SetURLVal(params, "yearlyrainin", u.YearlyRainIn)
return params
}

View File

@ -1,29 +1,29 @@
package wunderground package wunderground
type WundergroundUpdate struct { type WundergroundUpdate struct {
ID string `json:"ID,omitempty" schema:"ID"` ID *string `json:"ID,omitempty" schema:"ID"`
Password string `json:"PASSWORD,omitempty" schema:"PASSWORD"` Password *string `json:"PASSWORD,omitempty" schema:"PASSWORD"`
UV int `json:"UV,omitempty" schema:"UV"` UV *int `json:"UV,omitempty" schema:"UV"`
Action string `json:"action,omitempty" schema:"action"` Action *string `json:"action,omitempty" schema:"action"`
BaromIn float64 `json:"baromin,omitempty" schema:"baromin"` BaromIn *float64 `json:"baromin,omitempty" schema:"baromin"`
DailyRainIn float64 `json:"dailyrainin,omitempty" schema:"dailyrainin"` DailyRainIn *float64 `json:"dailyrainin,omitempty" schema:"dailyrainin"`
DateUTC string `json:"dateutc,omitempty" schema:"dateutc"` DateUTC *string `json:"dateutc,omitempty" schema:"dateutc"`
DewPtF float64 `json:"dewptf,omitempty" schema:"dewptf"` DewPtF *float64 `json:"dewptf,omitempty" schema:"dewptf"`
Humidity int `json:"humidity,omitempty" schema:"humidity"` Humidity *int `json:"humidity,omitempty" schema:"humidity"`
IndoorHumidity int `json:"indoorhumidity,omitempty" schema:"indoorhumidity"` IndoorHumidity *int `json:"indoorhumidity,omitempty" schema:"indoorhumidity"`
IndoorTempF float64 `json:"indoortempf,omitempty" schema:"indoortempf"` IndoorTempF *float64 `json:"indoortempf,omitempty" schema:"indoortempf"`
LowBatt bool `json:"lowbatt,omitempty" schema:"lowbatt"` LowBatt *bool `json:"lowbatt,omitempty" schema:"lowbatt"`
MonthlyRainIn float64 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"` MonthlyRainIn *float64 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"`
RainIn float64 `json:"rainin,omitempty" schema:"rainin"` RainIn *float64 `json:"rainin,omitempty" schema:"rainin"`
Realtime bool `json:"realtime,omitempty" schema:"realtime"` Realtime *bool `json:"realtime,omitempty" schema:"realtime"`
Rtfreq int `json:"rtfreq,omitempty" schema:"rtfreq"` Rtfreq *int `json:"rtfreq,omitempty" schema:"rtfreq"`
SoftwareType string `json:"softwaretype,omitempty" schema:"softwaretype"` SoftwareType *string `json:"softwaretype,omitempty" schema:"softwaretype"`
SolarRadiation float64 `json:"solarradiation,omitempty" schema:"solarradiation"` SolarRadiation *float64 `json:"solarradiation,omitempty" schema:"solarradiation"`
Tempf float64 `json:"tempf,omitempty" schema:"tempf"` Tempf *float64 `json:"tempf,omitempty" schema:"tempf"`
WeeklyRainIn float64 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"` WeeklyRainIn *float64 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"`
WindChillF float64 `json:"windchillf,omitempty" schema:"windchillf"` WindChillF *float64 `json:"windchillf,omitempty" schema:"windchillf"`
WindDir int `json:"winddir,omitempty" schema:"winddir"` WindDir *int `json:"winddir,omitempty" schema:"winddir"`
WindGustMPH float64 `json:"windgustmph,omitempty" schema:"windgustmph"` WindGustMPH *float64 `json:"windgustmph,omitempty" schema:"windgustmph"`
WindSpeedMPH float64 `json:"windspeedmph,omitempty" schema:"windspeedmph"` WindSpeedMPH *float64 `json:"windspeedmph,omitempty" schema:"windspeedmph"`
YearlyRainIn float64 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"` YearlyRainIn *float64 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"`
} }

View File

@ -1,19 +1,44 @@
package weather package weather
import "math" import (
"math"
"net/url"
"strconv"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
)
// Attempts to complete missing fields that may not // Attempts to complete missing fields that may not
// be set by a specific provider, such as DewPoint and WindChill // be set by a specific provider, such as DewPoint and WindChill
func (u *WeatherUpdate) Enrich() { func (u *WeatherUpdate) Enrich(weatherStations ...*config.WeatherStation) {
if u.WindChillF == 0 { if u == nil {
u.WindChillF = CalculateWindChill(u.TempOutdoorF, u.WindSpeedMPH) return
} }
if u.DewPointF == 0 { // Clear invalid measurements
u.DewPointF = CalculateDewPoint(u.TempOutdoorF, float64(u.HumidityOudoor)) if u.BaromAbsoluteIn != nil && *u.BaromAbsoluteIn < 20 {
u.BaromAbsoluteIn = nil
}
if u.BaromRelativeIn != nil && *u.BaromRelativeIn < 20 {
u.BaromRelativeIn = nil
} }
if u.BaromAbsoluteIn == 0 { // Calculate Wind Chill
if u.WindChillF == nil && u.TempOutdoorF != nil && u.WindSpeedMPH != nil {
wc := CalculateWindChill(*u.TempOutdoorF, *u.WindSpeedMPH)
u.WindChillF = &wc
}
// Calculate Dew Point
if u.DewPointF == nil && (u.TempOutdoorF != nil && u.HumidityOudoor != nil) {
if *u.TempOutdoorF != 0 || *u.HumidityOudoor != 0 {
dp := CalculateDewPoint(*u.TempOutdoorF, float64(*u.HumidityOudoor))
u.DewPointF = &dp
}
}
// Use relative pressure if absolute isn't provided
if u.BaromAbsoluteIn == nil && u.BaromRelativeIn != nil {
u.BaromAbsoluteIn = u.BaromRelativeIn u.BaromAbsoluteIn = u.BaromRelativeIn
} }
} }
@ -45,3 +70,31 @@ func CalculateWindChill(tempF float64, windSpeedMPH float64) float64 {
35.75*math.Pow(windSpeedMPH, 0.16) + 35.75*math.Pow(windSpeedMPH, 0.16) +
0.4275*tempF*math.Pow(windSpeedMPH, 0.16) 0.4275*tempF*math.Pow(windSpeedMPH, 0.16)
} }
// Helper function to set values from fields
// typically from a WeatherUpdate
func SetURLVal(vals *url.Values, key string, value any) {
if value == nil {
return
}
switch v := value.(type) {
case *float64:
if v == nil {
return
}
str := strconv.FormatFloat(*v, 'f', 4, 64)
vals.Set(key, str)
case *int:
if v == nil {
return
}
str := strconv.FormatInt(int64(*v), 10)
vals.Set(key, str)
case *string:
if v == nil {
return
}
vals.Set(key, *v)
}
}

View File

@ -5,6 +5,7 @@ import (
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0" semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
@ -12,137 +13,155 @@ import (
type WeatherMetrics struct { type WeatherMetrics struct {
// Weather Metrics // Weather Metrics
TempOutdoorF metric.Float64Gauge TempOutdoorF metric.Float64Gauge
TempIndoorF metric.Float64Gauge TempIndoorF metric.Float64Gauge
HumidityOudoor metric.Int64Gauge HumidityOudoor metric.Int64Gauge
HumidityIndoor metric.Int64Gauge HumidityIndoor metric.Int64Gauge
WindSpeedMPH metric.Float64Gauge WindSpeedMPH metric.Float64Gauge
WindGustMPH metric.Float64Gauge WindGustMPH metric.Float64Gauge
MaxDailyGust metric.Float64Gauge MaxDailyGust metric.Float64Gauge
WindDir metric.Int64Gauge WindDir metric.Int64Gauge
WindDirAvg10m metric.Int64Gauge WindDirAvg10m metric.Int64Gauge
UV metric.Int64Gauge UV metric.Int64Gauge
SolarRadiation metric.Float64Gauge SolarRadiation metric.Float64Gauge
HourlyRainIn metric.Float64Gauge HourlyRainIn metric.Float64Gauge
EventRainIn metric.Float64Gauge EventRainIn metric.Float64Gauge
DailyRainIn metric.Float64Gauge DailyRainIn metric.Float64Gauge
WeeklyRainIn metric.Float64Gauge WeeklyRainIn metric.Float64Gauge
MonthlyRainIn metric.Float64Gauge MonthlyRainIn metric.Float64Gauge
YearlyRainIn metric.Float64Gauge YearlyRainIn metric.Float64Gauge
TotalRainIn metric.Float64Gauge TotalRainIn metric.Float64Gauge
BattOutdoorSensor metric.Int64Gauge BatteryStatus metric.Int64Gauge
BattIndoorSensor metric.Int64Gauge BaromRelativeIn metric.Float64Gauge
BattRainSensor metric.Int64Gauge BaromAbsoluteIn metric.Float64Gauge
BaromRelativeIn metric.Float64Gauge DewPointF metric.Float64Gauge
BaromAbsoluteIn metric.Float64Gauge WindChillF metric.Float64Gauge
DewPointF metric.Float64Gauge
WindChillF metric.Float64Gauge
// Internal Telemetry // Internal Telemetry
UpdatesReceived metric.Int64Counter UpdatesReceived metric.Int64Counter
appCtx context.Context appCtx context.Context
cfg *config.AppConfig cfg *config.AppConfig
meter metric.Meter meter metric.Meter
recorder *MetricRecorder
} }
var MetricPrefix = "weather"
func MustInitMetrics(appCtx context.Context) *WeatherMetrics { func MustInitMetrics(appCtx context.Context) *WeatherMetrics {
wm := &WeatherMetrics{ wm := &WeatherMetrics{
appCtx: appCtx, appCtx: appCtx,
cfg: config.MustFromCtx(appCtx), cfg: config.MustFromCtx(appCtx),
recorder: &MetricRecorder{ctx: appCtx, l: zerolog.Ctx(appCtx)},
} }
wm.meter = otel.GetMeter(appCtx, "weather", "metrics") wm.meter = otel.GetMeter(appCtx, "weather", "metrics")
// Weather Metrics // Weather Metrics
wm.TempOutdoorF, _ = wm.meter.Float64Gauge("weather_temp_outdoor_f", wm.TempOutdoorF, _ = wm.meter.Float64Gauge(MetricPrefix+"_temp_outdoor_f",
metric.WithDescription("Outdoor Temperature in Faherenheit")) metric.WithDescription("Outdoor Temperature in Faherenheit"))
wm.TempIndoorF, _ = wm.meter.Float64Gauge("weather_temp_indoor_f", wm.TempIndoorF, _ = wm.meter.Float64Gauge(MetricPrefix+"_temp_indoor_f",
metric.WithDescription("Indoor Temperature in Faherenheit")) metric.WithDescription("Indoor Temperature in Faherenheit"))
wm.HumidityOudoor, _ = wm.meter.Int64Gauge("weather_humidity_oudoor", wm.HumidityOudoor, _ = wm.meter.Int64Gauge(MetricPrefix+"_humidity_outdoor",
metric.WithDescription("Outdoor Humidity %")) metric.WithDescription("Outdoor Humidity %"))
wm.HumidityIndoor, _ = wm.meter.Int64Gauge("weather_humidity_indoor", wm.HumidityIndoor, _ = wm.meter.Int64Gauge(MetricPrefix+"_humidity_indoor",
metric.WithDescription("Indoor Humidity %")) metric.WithDescription("Indoor Humidity %"))
wm.WindSpeedMPH, _ = wm.meter.Float64Gauge("weather_wind_speed_mph", wm.WindSpeedMPH, _ = wm.meter.Float64Gauge(MetricPrefix+"_wind_speed_mph",
metric.WithDescription("Wind Speed in MPH")) metric.WithDescription("Wind Speed in MPH"))
wm.WindGustMPH, _ = wm.meter.Float64Gauge("weather_wind_gust_mph", wm.WindGustMPH, _ = wm.meter.Float64Gauge(MetricPrefix+"_wind_gust_mph",
metric.WithDescription("Wind Gust in MPH")) metric.WithDescription("Wind Gust in MPH"))
wm.MaxDailyGust, _ = wm.meter.Float64Gauge("weather_max_daily_gust", wm.MaxDailyGust, _ = wm.meter.Float64Gauge(MetricPrefix+"_max_daily_gust",
metric.WithDescription("Max Daily Wind Gust")) metric.WithDescription("Max Daily Wind Gust"))
wm.WindDir, _ = wm.meter.Int64Gauge("weather_wind_dir", wm.WindDir, _ = wm.meter.Int64Gauge(MetricPrefix+"_wind_dir",
metric.WithDescription("Wind Direction in Degrees")) metric.WithDescription("Wind Direction in Degrees"))
wm.WindDirAvg10m, _ = wm.meter.Int64Gauge("weather_wind_dir_avg_10m", wm.WindDirAvg10m, _ = wm.meter.Int64Gauge(MetricPrefix+"_wind_dir_avg_10m",
metric.WithDescription("Wind Direction 10m Average")) metric.WithDescription("Wind Direction 10m Average"))
wm.UV, _ = wm.meter.Int64Gauge("weather_uv", wm.UV, _ = wm.meter.Int64Gauge(MetricPrefix+"_uv",
metric.WithDescription("UV Index")) metric.WithDescription("UV Index"))
wm.SolarRadiation, _ = wm.meter.Float64Gauge("weather_solar_radiation", wm.SolarRadiation, _ = wm.meter.Float64Gauge(MetricPrefix+"_solar_radiation",
metric.WithDescription("Solar Radiation in W/㎡")) metric.WithDescription("Solar Radiation in W/㎡"))
wm.HourlyRainIn, _ = wm.meter.Float64Gauge("weather_hourly_rain_in", wm.HourlyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_hourly_rain_in",
metric.WithDescription("Hourly Rain in Inches")) metric.WithDescription("Hourly Rain in Inches"))
wm.EventRainIn, _ = wm.meter.Float64Gauge("weather_event_rain_in", wm.EventRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_event_rain_in",
metric.WithDescription("Event Rain in Inches")) metric.WithDescription("Event Rain in Inches"))
wm.DailyRainIn, _ = wm.meter.Float64Gauge("weather_daily_rain_in", wm.DailyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_daily_rain_in",
metric.WithDescription("Daily Rain in Inches")) metric.WithDescription("Daily Rain in Inches"))
wm.WeeklyRainIn, _ = wm.meter.Float64Gauge("weather_weekly_rain_in", wm.WeeklyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_weekly_rain_in",
metric.WithDescription("Weekly Rain in Inches")) metric.WithDescription("Weekly Rain in Inches"))
wm.MonthlyRainIn, _ = wm.meter.Float64Gauge("weather_monthly_rain_in", wm.MonthlyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_monthly_rain_in",
metric.WithDescription("Monthly Rain in Inches")) metric.WithDescription("Monthly Rain in Inches"))
wm.YearlyRainIn, _ = wm.meter.Float64Gauge("weather_yearly_rain_in", wm.YearlyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_yearly_rain_in",
metric.WithDescription("Yearly Rain in Inches")) metric.WithDescription("Yearly Rain in Inches"))
wm.TotalRainIn, _ = wm.meter.Float64Gauge("weather_total_rain_in", wm.TotalRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_total_rain_in",
metric.WithDescription("Total Rain in Inches")) metric.WithDescription("Total Rain in Inches"))
wm.BattOutdoorSensor, _ = wm.meter.Int64Gauge("weather_batt_outdoor_sensor", wm.BatteryStatus, _ = wm.meter.Int64Gauge(MetricPrefix+"_battery_status",
metric.WithDescription("Outdoor Equipment Battery")) metric.WithDescription("Per-component battery status"))
wm.BattIndoorSensor, _ = wm.meter.Int64Gauge("weather_batt_indoor_sensor", wm.BaromRelativeIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_barometric_pressure_relative_in",
metric.WithDescription("Indoor Equipmenet Battery"))
wm.BattRainSensor, _ = wm.meter.Int64Gauge("weather_batt_rain_sensor",
metric.WithDescription("Rain Sensor Battery"))
wm.BaromRelativeIn, _ = wm.meter.Float64Gauge("weather_barometric_pressure_relative_in",
metric.WithDescription("Relative Pressure in Inches of Mercury")) metric.WithDescription("Relative Pressure in Inches of Mercury"))
wm.BaromAbsoluteIn, _ = wm.meter.Float64Gauge("weather_barometric_pressure_absolute_in", wm.BaromAbsoluteIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_barometric_pressure_absolute_in",
metric.WithDescription("Absolute Pressure in Inches of Mercury")) metric.WithDescription("Absolute Pressure in Inches of Mercury"))
wm.DewPointF, _ = wm.meter.Float64Gauge("weather_dew_point_f", wm.DewPointF, _ = wm.meter.Float64Gauge(MetricPrefix+"_dew_point_f",
metric.WithDescription("Dew Point in Faherenheit")) metric.WithDescription("Dew Point in Faherenheit"))
wm.WindChillF, _ = wm.meter.Float64Gauge("weather_wind_chill_f", wm.WindChillF, _ = wm.meter.Float64Gauge(MetricPrefix+"_wind_chill_f",
metric.WithDescription("Wind Chill in Faherenheit")) metric.WithDescription("Wind Chill in Faherenheit"))
// Internal Telemetry // Internal Telemetry
wm.UpdatesReceived, _ = wm.meter.Int64Counter("weather_updates_received", wm.UpdatesReceived, _ = wm.meter.Int64Counter(MetricPrefix+"_updates_received",
metric.WithDescription("Metric Updates Processed by Exporter")) metric.WithDescription("Metric Updates Processed by Exporter"))
return wm return wm
} }
func (wm *WeatherMetrics) Update(u *WeatherUpdate) { func (wm *WeatherMetrics) Update(u *WeatherUpdate) {
attributes := attribute.NewSet( attributes := []attribute.KeyValue{
semconv.ServiceVersion(wm.cfg.Version), semconv.ServiceVersion(wm.cfg.Version),
attribute.String("station_type", u.StationType), semconv.ServiceName(wm.cfg.Name),
) semconv.DeploymentEnvironment(wm.cfg.Environment),
}
if u.StationType != nil {
attributes = append(attributes,
attribute.String("station_type", *u.StationType))
}
if u.StationConfig != nil {
if u.StationConfig.Name != "" {
attributes = append(attributes,
attribute.String("station_name", u.StationConfig.Name))
}
if u.StationConfig.Equipment != "" {
attributes = append(attributes,
attribute.String("station_equipment", u.StationConfig.Equipment))
}
}
wm.TempOutdoorF.Record(wm.appCtx, u.TempOutdoorF, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TempOutdoorF, FloatVal: u.TempOutdoorF, Field: FieldTempOutdoorF, Attributes: attributes, Station: u.StationConfig})
wm.TempIndoorF.Record(wm.appCtx, u.TempIndoorF, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TempIndoorF, FloatVal: u.TempIndoorF, Field: FieldTempIndoorF, Attributes: attributes, Station: u.StationConfig})
wm.HumidityOudoor.Record(wm.appCtx, int64(u.HumidityOudoor), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Int64Gauge: wm.HumidityOudoor, IntVal: u.HumidityOudoor, Field: FieldHumidityOudoor, Attributes: attributes, Station: u.StationConfig})
wm.HumidityIndoor.Record(wm.appCtx, int64(u.HumidityIndoor), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Int64Gauge: wm.HumidityIndoor, IntVal: u.HumidityIndoor, Field: FieldHumidityIndoor, Attributes: attributes, Station: u.StationConfig})
wm.WindSpeedMPH.Record(wm.appCtx, u.WindSpeedMPH, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindSpeedMPH, FloatVal: u.WindSpeedMPH, Field: FieldWindSpeedMPH, Attributes: attributes, Station: u.StationConfig})
wm.WindGustMPH.Record(wm.appCtx, u.WindGustMPH, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindGustMPH, FloatVal: u.WindGustMPH, Field: FieldWindGustMPH, Attributes: attributes, Station: u.StationConfig})
wm.MaxDailyGust.Record(wm.appCtx, u.MaxDailyGust, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.MaxDailyGust, FloatVal: u.MaxDailyGust, Field: FieldMaxDailyGust, Attributes: attributes, Station: u.StationConfig})
wm.WindDir.Record(wm.appCtx, int64(u.WindDir), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Int64Gauge: wm.WindDir, IntVal: u.WindDir, Field: FieldWindDir, Attributes: attributes, Station: u.StationConfig})
wm.WindDirAvg10m.Record(wm.appCtx, int64(u.WindDirAvg10m), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Int64Gauge: wm.WindDirAvg10m, IntVal: u.WindDirAvg10m, Field: FieldWindDirAvg10m, Attributes: attributes, Station: u.StationConfig})
wm.UV.Record(wm.appCtx, int64(u.UV), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Int64Gauge: wm.UV, IntVal: u.UV, Field: FieldUV, Attributes: attributes, Station: u.StationConfig})
wm.SolarRadiation.Record(wm.appCtx, u.SolarRadiation, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.SolarRadiation, FloatVal: u.SolarRadiation, Field: FieldSolarRadiation, Attributes: attributes, Station: u.StationConfig})
wm.HourlyRainIn.Record(wm.appCtx, u.HourlyRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.HourlyRainIn, FloatVal: u.HourlyRainIn, Field: FieldHourlyRainIn, Attributes: attributes, Station: u.StationConfig})
wm.EventRainIn.Record(wm.appCtx, u.EventRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.EventRainIn, FloatVal: u.EventRainIn, Field: FieldEventRainIn, Attributes: attributes, Station: u.StationConfig})
wm.DailyRainIn.Record(wm.appCtx, u.DailyRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.DailyRainIn, FloatVal: u.DailyRainIn, Field: FieldDailyRainIn, Attributes: attributes, Station: u.StationConfig})
wm.WeeklyRainIn.Record(wm.appCtx, u.WeeklyRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WeeklyRainIn, FloatVal: u.WeeklyRainIn, Field: FieldWeeklyRainIn, Attributes: attributes, Station: u.StationConfig})
wm.MonthlyRainIn.Record(wm.appCtx, u.MonthlyRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.MonthlyRainIn, FloatVal: u.MonthlyRainIn, Field: FieldMonthlyRainIn, Attributes: attributes, Station: u.StationConfig})
wm.YearlyRainIn.Record(wm.appCtx, u.YearlyRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.YearlyRainIn, FloatVal: u.YearlyRainIn, Field: FieldYearlyRainIn, Attributes: attributes, Station: u.StationConfig})
wm.TotalRainIn.Record(wm.appCtx, u.TotalRainIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TotalRainIn, FloatVal: u.TotalRainIn, Field: FieldTotalRainIn, Attributes: attributes, Station: u.StationConfig})
wm.BattOutdoorSensor.Record(wm.appCtx, int64(u.BattOutdoorSensor), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.BaromRelativeIn, FloatVal: u.BaromRelativeIn, Field: FieldBaromRelativeIn, Attributes: attributes, Station: u.StationConfig})
wm.BattIndoorSensor.Record(wm.appCtx, int64(u.BattIndoorSensor), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.BaromAbsoluteIn, FloatVal: u.BaromAbsoluteIn, Field: FieldBaromAbsoluteIn, Attributes: attributes, Station: u.StationConfig})
wm.BattRainSensor.Record(wm.appCtx, int64(u.BattRainSensor), metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.DewPointF, FloatVal: u.DewPointF, Field: FieldDewPointF, Attributes: attributes, Station: u.StationConfig})
wm.BaromRelativeIn.Record(wm.appCtx, u.BaromRelativeIn, metric.WithAttributeSet(attributes)) wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindChillF, FloatVal: u.WindChillF, Field: FieldWindChillF, Attributes: attributes, Station: u.StationConfig})
wm.BaromAbsoluteIn.Record(wm.appCtx, u.BaromAbsoluteIn, metric.WithAttributeSet(attributes))
wm.DewPointF.Record(wm.appCtx, u.DewPointF, metric.WithAttributeSet(attributes)) // Batteries
wm.WindChillF.Record(wm.appCtx, u.WindChillF, metric.WithAttributeSet(attributes)) for _, battery := range u.Batteries {
batAttr := attributes
batAttr = append(batAttr, attribute.String("component", battery.Component))
wm.recorder.Record(&RecordOpts{Int64Gauge: wm.BatteryStatus, IntVal: battery.Status, Field: FieldBatteries, Attributes: batAttr, Station: u.StationConfig})
}
wm.UpdatesReceived.Add(wm.appCtx, 1) wm.UpdatesReceived.Add(wm.appCtx, 1)
} }

View File

@ -0,0 +1,106 @@
package weather
import (
"context"
"errors"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
)
type MetricRecorder struct {
ctx context.Context
l *zerolog.Logger
}
type RecordOpts struct {
Float64Gauge metric.Float64Gauge
Int64Gauge metric.Int64Gauge
IntVal *int
FloatVal *float64
Attributes []attribute.KeyValue
Field string
Station *config.WeatherStation
}
func (r *MetricRecorder) Record(opts *RecordOpts) {
if opts.Station != nil && !opts.keep() {
r.l.Trace().
Str("field", string(opts.Field)).
Str("station", opts.Station.Name).
Msg("Metric dropped by station config")
return
} else if opts.Int64Gauge == nil && opts.Float64Gauge == nil {
r.l.Err(errors.New("neither int nor float gauge provided")).Send()
return
}
if opts.Int64Gauge != nil {
if opts.IntVal == nil {
log := r.l.Trace().Str("field", string(opts.Field))
if opts.Station != nil {
log = log.Str("station", opts.Station.Name)
}
log.Msg("Dropping nil int metric")
return
}
r.recordInt(opts.Int64Gauge, *opts.IntVal, opts.Attributes...)
} else if opts.Float64Gauge != nil {
if opts.FloatVal == nil {
log := r.l.Trace().Str("field", string(opts.Field))
if opts.Station != nil {
log = log.Str("station", opts.Station.Name)
}
log.Msg("Dropping nil float metric")
return
}
r.recordFloat(opts.Float64Gauge, *opts.FloatVal, opts.Attributes...)
}
}
func (o *RecordOpts) keep() bool {
// If keep fields are given, only check keep fields
if len(o.Station.KeepMetrics) > 0 {
for _, f := range o.Station.KeepMetrics {
if f == o.Field {
return true
}
}
return false
}
for _, f := range o.Station.DropMetrics {
if f == o.Field {
return false
}
}
return true
}
func (r *MetricRecorder) recordInt(
m metric.Int64Gauge, value int, attributes ...attribute.KeyValue,
) {
// Prepare metric attributes
options := make([]metric.RecordOption, 0, len(attributes))
if len(attributes) > 0 {
options = append(options, metric.WithAttributes(attributes...))
}
val := int64(value)
m.Record(r.ctx, val, options...)
}
func (r *MetricRecorder) recordFloat(
m metric.Float64Gauge, value float64, attributes ...attribute.KeyValue,
) {
// Prepare metric attributes
options := make([]metric.RecordOption, 0, len(attributes))
if len(attributes) > 0 {
options = append(options, metric.WithAttributes(attributes...))
}
m.Record(r.ctx, value, options...)
}

View File

@ -2,38 +2,85 @@ package weather
import ( import (
"time" "time"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
) )
// Stable intermediate struct containing superset of fields // Stable intermediate struct containing superset of fields
// between AWN and Wunderground style updates from Ambient devices // between AWN and Wunderground style updates from Ambient devices
type WeatherUpdate struct { type WeatherUpdate struct {
DateUTC *time.Time DateUTC *time.Time
StationType string StationConfig *config.WeatherStation
TempOutdoorF float64 StationID *string
TempIndoorF float64 StationType *string
HumidityOudoor int TempOutdoorF *float64
HumidityIndoor int TempIndoorF *float64
WindSpeedMPH float64 HumidityOudoor *int
WindGustMPH float64 HumidityIndoor *int
MaxDailyGust float64 WindSpeedMPH *float64
WindDir int WindGustMPH *float64
WindDirAvg10m int MaxDailyGust *float64
UV int WindDir *int
SolarRadiation float64 WindDirAvg10m *int
HourlyRainIn float64 UV *int
EventRainIn float64 SolarRadiation *float64
DailyRainIn float64 HourlyRainIn *float64
WeeklyRainIn float64 EventRainIn *float64
MonthlyRainIn float64 DailyRainIn *float64
YearlyRainIn float64 WeeklyRainIn *float64
TotalRainIn float64 MonthlyRainIn *float64
BattOutdoorSensor int YearlyRainIn *float64
BattIndoorSensor int TotalRainIn *float64
BattRainSensor int Batteries []BatteryStatus
BaromRelativeIn float64 BaromRelativeIn *float64
BaromAbsoluteIn float64 BaromAbsoluteIn *float64
// These fields may be calculated // These fields may be calculated
// if not otherwise set // if not otherwise set
DewPointF float64 DewPointF *float64
WindChillF float64 WindChillF *float64
// First URL parameters given to AWN/Wunderground
// if proxying is enabled
}
type BatteryStatus struct {
Component string
Status *int
}
// CHORE: Maintain this, used to check against
// keep and drop lists
// TODO: Use refelct/ast to generate code
const (
FieldDateUTC = "DateUTC"
FieldStationType = "StationType"
FieldTempOutdoorF = "TempOutdoorF"
FieldTempIndoorF = "TempIndoorF"
FieldHumidityOudoor = "HumidityOudoor"
FieldHumidityIndoor = "HumidityIndoor"
FieldWindSpeedMPH = "WindSpeedMPH"
FieldWindGustMPH = "WindGustMPH"
FieldMaxDailyGust = "MaxDailyGust"
FieldWindDir = "WindDir"
FieldWindDirAvg10m = "WindDirAvg10m"
FieldUV = "UV"
FieldSolarRadiation = "SolarRadiation"
FieldHourlyRainIn = "HourlyRainIn"
FieldEventRainIn = "EventRainIn"
FieldDailyRainIn = "DailyRainIn"
FieldWeeklyRainIn = "WeeklyRainIn"
FieldMonthlyRainIn = "MonthlyRainIn"
FieldYearlyRainIn = "YearlyRainIn"
FieldTotalRainIn = "TotalRainIn"
FieldBatteries = "Batteries"
FieldBaromRelativeIn = "BaromRelativeIn"
FieldBaromAbsoluteIn = "BaromAbsoluteIn"
FieldDewPointF = "DewPointF"
FieldWindChillF = "WindChillF"
)
func (u *WeatherUpdate) GetStationName() string {
if u.StationConfig != nil {
return u.StationConfig.Name
}
return ""
} }