Compare commits

...

21 Commits

Author SHA1 Message Date
00f61879c1 Support metric prefix change
All checks were successful
Build and Publish / release (push) Successful in 3m43s
2025-01-08 17:02:58 -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
14 changed files with 276 additions and 134 deletions

4
.gitignore vendored
View File

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

21
TODO.md
View File

@ -1,10 +1,19 @@
# TODO
- [ ] Fix shutdown
- [ ] Configuration for app
- [ ] Makefile
- [ ] Dockerfile
- [ ] Configurable metric prefix
- [ ] Helm Chart
- [ ] Gitea CI
- [ ] Update README
- [ ] Version flag
- [ ] Add Grafana dashboard
- [ ] Add device name field with ID/Key mappings
- [ ] Add device type field with ID/Key mappings
- [ ] Move EVERYTHING to pointers to support nil
- [ ] Add proxy to upstream support
## Done
- [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_TIME_FORMAT: rfc3339 ## long, short, unix, rfc3339, off
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_METRIC_INTERVAL_SECS: 30
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel.libretechconsulting.com:4317 # Set to your otel collector

14
go.mod
View File

@ -3,7 +3,7 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter
go 1.23.4
require (
gitea.libretechconsulting.com/rmcguire/go-app v0.1.1
gitea.libretechconsulting.com/rmcguire/go-app v0.3.1
github.com/gorilla/schema v1.4.1
github.com/rs/zerolog v1.33.0
go.opentelemetry.io/otel v1.33.0
@ -13,7 +13,7 @@ require (
require (
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/cespare/xxhash/v2 v2.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@ -40,12 +40,12 @@ require (
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
golang.org/x/net v0.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/net v0.34.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/rpc v0.0.0-20250102185135-69823020774d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.1 // indirect
google.golang.org/protobuf v1.36.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

22
go.sum
View File

@ -1,11 +1,11 @@
gitea.libretechconsulting.com/rmcguire/go-app v0.1.0 h1:H4TMgQ463oRNOyoi0FAvfGtOoDn651zNZStxM+sdNuU=
gitea.libretechconsulting.com/rmcguire/go-app v0.1.0/go.mod h1:p0ajkpFvzzD6VZ4xSjuowtwGRb1DjMfo/iG6LyFqFCs=
gitea.libretechconsulting.com/rmcguire/go-app v0.1.1 h1:Hrxqi1tqz8mf0baBsWgFe/S4jyMtIuPqH2FlanJUMNc=
gitea.libretechconsulting.com/rmcguire/go-app v0.1.1/go.mod h1:p0ajkpFvzzD6VZ4xSjuowtwGRb1DjMfo/iG6LyFqFCs=
gitea.libretechconsulting.com/rmcguire/go-app v0.3.0 h1:TSR6oEDBX+83975gmgGgU/cTFgfG999+9N/1h4RAXq0=
gitea.libretechconsulting.com/rmcguire/go-app v0.3.0/go.mod h1:wHOWh4O4AMDATQ3WEUYjq5a5bnICPBpu5G6BsNxqN38=
gitea.libretechconsulting.com/rmcguire/go-app v0.3.1 h1:jydcJ+Vv8sk+Le7nTI2+b6E7FfV+ShBJo7YdxmdaCYc=
gitea.libretechconsulting.com/rmcguire/go-app v0.3.1/go.mod h1:wHOWh4O4AMDATQ3WEUYjq5a5bnICPBpu5G6BsNxqN38=
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/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
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/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@ -93,10 +93,14 @@ go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qq
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -106,12 +110,18 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
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=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

19
main.go
View File

@ -12,15 +12,26 @@ import (
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient"
)
const defaultMetricPrefix = "weather"
func main() {
ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, unix.SIGTERM)
defer cncl()
ctx = app.MustSetupConfigAndLogging(ctx)
// Config type for app, which implements go-app/config.AppConfig
awConfig := &ambient.AmbientLocalExporterConfig{
MetricPrefix: defaultMetricPrefix,
WeatherStations: make([]ambient.WeatherStation, 0),
}
aw := ambient.New(ctx)
aw.MustInit()
// Read config and environment, set up logging, load up
// 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{
AppContext: ctx,
HTTP: &app.AppHTTP{
@ -43,7 +54,7 @@ func main() {
},
}
// Run and wait
awApp.MustRun()
<-awApp.Done()
}

View File

@ -24,6 +24,7 @@ type AmbientWeather struct {
// when either "AmbientWeather" or "Wunderground" are selected
// in the "Custom" section of the AWNet app, or the web UI
// of an Ambient WeatherHub
config *AmbientLocalExporterConfig
awnProvider provider.AmbientProvider
wuProvider provider.AmbientProvider
appCtx context.Context
@ -31,16 +32,18 @@ type AmbientWeather struct {
l *zerolog.Logger
}
func New(appCtx context.Context) *AmbientWeather {
func New(appCtx context.Context, awConfig *AmbientLocalExporterConfig) *AmbientWeather {
return &AmbientWeather{
appCtx: appCtx,
}
}
func (aw *AmbientWeather) MustInit() {
// Initialize with defaults, set logger from context
func (aw *AmbientWeather) Init() *AmbientWeather {
aw.awnProvider = &awn.AWNProvider{}
aw.wuProvider = &wunderground.WUProvider{}
aw.l = zerolog.Ctx(aw.appCtx)
return aw
}
func (aw *AmbientWeather) GetAWNHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) {
@ -55,7 +58,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
// any missing fields as the two providers supported by Ambient
// devices (awn/wunderground) produce different fields
@ -85,6 +88,7 @@ func (aw *AmbientWeather) handleProviderRequest(
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
// Calculate any fields that may be missing
@ -93,6 +97,9 @@ func (aw *AmbientWeather) handleProviderRequest(
// Update metrics
if aw.metrics == nil {
if aw.config.MetricPrefix != "" {
weather.MetricPrefix = aw.config.MetricPrefix
}
aw.metrics = weather.MustInitMetrics(aw.appCtx)
}
aw.metrics.Update(update)

33
pkg/ambient/config.go Normal file
View File

@ -0,0 +1,33 @@
package ambient
import (
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/weather"
)
// 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)
// One of these is required, based on the type
// set in the "Custom" weather service on your
// console, weather station, or weather hub
WundergroundID string `yaml:"wundergroundID"`
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
// These fields are mapped from the provider fields
DiscardMetrics []weather.WeatherUpdateField `yaml:"discardMetrics"`
}

View File

@ -39,31 +39,46 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate {
}
return &weather.WeatherUpdate{
StationType: awnUpdate.StationType,
DateUTC: &updateTime,
TempOutdoorF: awnUpdate.TempF,
HumidityOudoor: awnUpdate.Humidity,
WindSpeedMPH: awnUpdate.WindGustMPH,
WindGustMPH: awnUpdate.WindGustMPH,
MaxDailyGust: awnUpdate.MaxDailyGust,
WindDir: awnUpdate.WindDir,
WindDirAvg10m: awnUpdate.WindDirAVG10m,
UV: awnUpdate.UV,
SolarRadiation: awnUpdate.SolarRadiation,
HourlyRainIn: awnUpdate.HourlyRainIn,
EventRainIn: awnUpdate.EventRainIn,
DailyRainIn: awnUpdate.DailyRainIn,
WeeklyRainIn: awnUpdate.WeeklyRainIn,
MonthlyRainIn: awnUpdate.MonthlyRainIn,
YearlyRainIn: awnUpdate.YearlyRainIn,
TotalRainIn: awnUpdate.TotalRainIn,
BattOutdoorSensor: awnUpdate.BattOut,
BattIndoorSensor: awnUpdate.BattIn,
BattRainSensor: awnUpdate.BattRain,
TempIndoorF: awnUpdate.TempInF,
HumidityIndoor: awnUpdate.HumidityIn,
BaromRelativeIn: awnUpdate.BaromRelIn,
BaromAbsoluteIn: awnUpdate.BaromAbsIn,
StationType: awnUpdate.StationType,
DateUTC: &updateTime,
TempOutdoorF: awnUpdate.TempF,
HumidityOudoor: awnUpdate.Humidity,
WindSpeedMPH: awnUpdate.WindSpeedMPH,
WindGustMPH: awnUpdate.WindGustMPH,
MaxDailyGust: awnUpdate.MaxDailyGust,
WindDir: awnUpdate.WindDir,
WindDirAvg10m: awnUpdate.WindDirAVG10m,
UV: awnUpdate.UV,
SolarRadiation: awnUpdate.SolarRadiation,
HourlyRainIn: awnUpdate.HourlyRainIn,
EventRainIn: awnUpdate.EventRainIn,
DailyRainIn: awnUpdate.DailyRainIn,
WeeklyRainIn: awnUpdate.WeeklyRainIn,
MonthlyRainIn: awnUpdate.MonthlyRainIn,
YearlyRainIn: awnUpdate.YearlyRainIn,
TotalRainIn: awnUpdate.TotalRainIn,
Batteries: []weather.BatteryStatus{
{
Component: "OutdoorSensor",
Status: awnUpdate.BattOut,
},
{
Component: "IndoorSensor",
Status: awnUpdate.BattIn,
},
{
Component: "RainSensor",
Status: awnUpdate.BattRain,
},
{
Component: "CO2Sensor",
Status: awnUpdate.BattCO2,
},
},
TempIndoorF: awnUpdate.TempInF,
HumidityIndoor: awnUpdate.HumidityIn,
BaromRelativeIn: awnUpdate.BaromRelIn,
BaromAbsoluteIn: awnUpdate.BaromAbsIn,
}
}

View File

@ -27,4 +27,5 @@ type AmbientWeatherUpdate struct {
BaromRelIn float64 `json:"baromrelin,omitempty" schema:"baromrelin"`
BaromAbsIn float64 `json:"baromabsin,omitempty" schema:"baromabsin"`
BattIn int `json:"battin,omitempty" schema:"battin"`
BattCO2 int `json:"batt_co2,omitempty" schema:"batt_co2"`
}

View File

@ -5,11 +5,15 @@ import "math"
// Attempts to complete missing fields that may not
// be set by a specific provider, such as DewPoint and WindChill
func (u *WeatherUpdate) Enrich() {
if u == nil {
return
}
if u.WindChillF == 0 {
u.WindChillF = CalculateWindChill(u.TempOutdoorF, u.WindSpeedMPH)
}
if u.DewPointF == 0 {
if u.DewPointF == 0 && (u.TempOutdoorF != 0 && u.HumidityOudoor != 0) {
u.DewPointF = CalculateDewPoint(u.TempOutdoorF, float64(u.HumidityOudoor))
}

View File

@ -12,31 +12,29 @@ import (
type WeatherMetrics struct {
// Weather Metrics
TempOutdoorF metric.Float64Gauge
TempIndoorF metric.Float64Gauge
HumidityOudoor metric.Int64Gauge
HumidityIndoor metric.Int64Gauge
WindSpeedMPH metric.Float64Gauge
WindGustMPH metric.Float64Gauge
MaxDailyGust metric.Float64Gauge
WindDir metric.Int64Gauge
WindDirAvg10m metric.Int64Gauge
UV metric.Int64Gauge
SolarRadiation metric.Float64Gauge
HourlyRainIn metric.Float64Gauge
EventRainIn metric.Float64Gauge
DailyRainIn metric.Float64Gauge
WeeklyRainIn metric.Float64Gauge
MonthlyRainIn metric.Float64Gauge
YearlyRainIn metric.Float64Gauge
TotalRainIn metric.Float64Gauge
BattOutdoorSensor metric.Int64Gauge
BattIndoorSensor metric.Int64Gauge
BattRainSensor metric.Int64Gauge
BaromRelativeIn metric.Float64Gauge
BaromAbsoluteIn metric.Float64Gauge
DewPointF metric.Float64Gauge
WindChillF metric.Float64Gauge
TempOutdoorF metric.Float64Gauge
TempIndoorF metric.Float64Gauge
HumidityOudoor metric.Int64Gauge
HumidityIndoor metric.Int64Gauge
WindSpeedMPH metric.Float64Gauge
WindGustMPH metric.Float64Gauge
MaxDailyGust metric.Float64Gauge
WindDir metric.Int64Gauge
WindDirAvg10m metric.Int64Gauge
UV metric.Int64Gauge
SolarRadiation metric.Float64Gauge
HourlyRainIn metric.Float64Gauge
EventRainIn metric.Float64Gauge
DailyRainIn metric.Float64Gauge
WeeklyRainIn metric.Float64Gauge
MonthlyRainIn metric.Float64Gauge
YearlyRainIn metric.Float64Gauge
TotalRainIn metric.Float64Gauge
BatteryStatus metric.Int64Gauge
BaromRelativeIn metric.Float64Gauge
BaromAbsoluteIn metric.Float64Gauge
DewPointF metric.Float64Gauge
WindChillF metric.Float64Gauge
// Internal Telemetry
UpdatesReceived metric.Int64Counter
@ -45,6 +43,8 @@ type WeatherMetrics struct {
meter metric.Meter
}
var MetricPrefix = "weather"
func MustInitMetrics(appCtx context.Context) *WeatherMetrics {
wm := &WeatherMetrics{
appCtx: appCtx,
@ -54,59 +54,55 @@ func MustInitMetrics(appCtx context.Context) *WeatherMetrics {
wm.meter = otel.GetMeter(appCtx, "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"))
wm.TempIndoorF, _ = wm.meter.Float64Gauge("weather_temp_indoor_f",
wm.TempIndoorF, _ = wm.meter.Float64Gauge(MetricPrefix+"_temp_indoor_f",
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 %"))
wm.HumidityIndoor, _ = wm.meter.Int64Gauge("weather_humidity_indoor",
wm.HumidityIndoor, _ = wm.meter.Int64Gauge(MetricPrefix+"_humidity_indoor",
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"))
wm.WindGustMPH, _ = wm.meter.Float64Gauge("weather_wind_gust_mph",
wm.WindGustMPH, _ = wm.meter.Float64Gauge(MetricPrefix+"_wind_gust_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"))
wm.WindDir, _ = wm.meter.Int64Gauge("weather_wind_dir",
wm.WindDir, _ = wm.meter.Int64Gauge(MetricPrefix+"_wind_dir",
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"))
wm.UV, _ = wm.meter.Int64Gauge("weather_uv",
wm.UV, _ = wm.meter.Int64Gauge(MetricPrefix+"_uv",
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/㎡"))
wm.HourlyRainIn, _ = wm.meter.Float64Gauge("weather_hourly_rain_in",
wm.HourlyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_hourly_rain_in",
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"))
wm.DailyRainIn, _ = wm.meter.Float64Gauge("weather_daily_rain_in",
wm.DailyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_daily_rain_in",
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"))
wm.MonthlyRainIn, _ = wm.meter.Float64Gauge("weather_monthly_rain_in",
wm.MonthlyRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_monthly_rain_in",
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"))
wm.TotalRainIn, _ = wm.meter.Float64Gauge("weather_total_rain_in",
wm.TotalRainIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_total_rain_in",
metric.WithDescription("Total Rain in Inches"))
wm.BattOutdoorSensor, _ = wm.meter.Int64Gauge("weather_batt_outdoor_sensor",
metric.WithDescription("Outdoor Equipment Battery"))
wm.BattIndoorSensor, _ = wm.meter.Int64Gauge("weather_batt_indoor_sensor",
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",
wm.BatteryStatus, _ = wm.meter.Int64Gauge(MetricPrefix+"_battery_status",
metric.WithDescription("Per-component battery status"))
wm.BaromRelativeIn, _ = wm.meter.Float64Gauge(MetricPrefix+"_barometric_pressure_relative_in",
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"))
wm.DewPointF, _ = wm.meter.Float64Gauge("weather_dew_point_f",
wm.DewPointF, _ = wm.meter.Float64Gauge(MetricPrefix+"_dew_point_f",
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"))
// 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"))
return wm
@ -136,13 +132,18 @@ func (wm *WeatherMetrics) Update(u *WeatherUpdate) {
wm.MonthlyRainIn.Record(wm.appCtx, u.MonthlyRainIn, metric.WithAttributeSet(attributes))
wm.YearlyRainIn.Record(wm.appCtx, u.YearlyRainIn, metric.WithAttributeSet(attributes))
wm.TotalRainIn.Record(wm.appCtx, u.TotalRainIn, metric.WithAttributeSet(attributes))
wm.BattOutdoorSensor.Record(wm.appCtx, int64(u.BattOutdoorSensor), metric.WithAttributeSet(attributes))
wm.BattIndoorSensor.Record(wm.appCtx, int64(u.BattIndoorSensor), metric.WithAttributeSet(attributes))
wm.BattRainSensor.Record(wm.appCtx, int64(u.BattRainSensor), metric.WithAttributeSet(attributes))
wm.BaromRelativeIn.Record(wm.appCtx, u.BaromRelativeIn, metric.WithAttributeSet(attributes))
wm.BaromAbsoluteIn.Record(wm.appCtx, u.BaromAbsoluteIn, metric.WithAttributeSet(attributes))
wm.DewPointF.Record(wm.appCtx, u.DewPointF, metric.WithAttributeSet(attributes))
wm.WindChillF.Record(wm.appCtx, u.WindChillF, metric.WithAttributeSet(attributes))
// Batteries
for _, battery := range u.Batteries {
wm.BatteryStatus.Record(wm.appCtx, int64(battery.Status),
metric.WithAttributeSet(attributes),
metric.WithAttributes(attribute.String("component", battery.Component)),
)
}
wm.UpdatesReceived.Add(wm.appCtx, 1)
}

View File

@ -7,33 +7,68 @@ import (
// Stable intermediate struct containing superset of fields
// between AWN and Wunderground style updates from Ambient devices
type WeatherUpdate struct {
DateUTC *time.Time
StationType string
TempOutdoorF float64
TempIndoorF float64
HumidityOudoor int
HumidityIndoor int
WindSpeedMPH float64
WindGustMPH float64
MaxDailyGust float64
WindDir int
WindDirAvg10m int
UV int
SolarRadiation float64
HourlyRainIn float64
EventRainIn float64
DailyRainIn float64
WeeklyRainIn float64
MonthlyRainIn float64
YearlyRainIn float64
TotalRainIn float64
BattOutdoorSensor int
BattIndoorSensor int
BattRainSensor int
BaromRelativeIn float64
BaromAbsoluteIn float64
DateUTC *time.Time
StationType string
TempOutdoorF float64
TempIndoorF float64
HumidityOudoor int
HumidityIndoor int
WindSpeedMPH float64
WindGustMPH float64
MaxDailyGust float64
WindDir int
WindDirAvg10m int
UV int
SolarRadiation float64
HourlyRainIn float64
EventRainIn float64
DailyRainIn float64
WeeklyRainIn float64
MonthlyRainIn float64
YearlyRainIn float64
TotalRainIn float64
Batteries []BatteryStatus
BaromRelativeIn float64
BaromAbsoluteIn float64
// These fields may be calculated
// if not otherwise set
DewPointF float64
WindChillF float64
}
type BatteryStatus struct {
Component string
Status int
}
type WeatherUpdateField string
// NOTE: Annoyance to avoid string constant comparisons
// CHORE: Maintain this
const (
FieldDateUTC WeatherUpdateField = "DateUTC"
FieldStationType WeatherUpdateField = "StationType"
FieldTempOutdoorF WeatherUpdateField = "TempOutdoorF"
FieldTempIndoorF WeatherUpdateField = "TempIndoorF"
FieldHumidityOudoor WeatherUpdateField = "HumidityOudoor"
FieldHumidityIndoor WeatherUpdateField = "HumidityIndoor"
FieldWindSpeedMPH WeatherUpdateField = "WindSpeedMPH"
FieldWindGustMPH WeatherUpdateField = "WindGustMPH"
FieldMaxDailyGust WeatherUpdateField = "MaxDailyGust"
FieldWindDir WeatherUpdateField = "WindDir"
FieldWindDirAvg10m WeatherUpdateField = "WindDirAvg10m"
FieldUV WeatherUpdateField = "UV"
FieldSolarRadiation WeatherUpdateField = "SolarRadiation"
FieldHourlyRainIn WeatherUpdateField = "HourlyRainIn"
FieldEventRainIn WeatherUpdateField = "EventRainIn"
FieldDailyRainIn WeatherUpdateField = "DailyRainIn"
FieldWeeklyRainIn WeatherUpdateField = "WeeklyRainIn"
FieldMonthlyRainIn WeatherUpdateField = "MonthlyRainIn"
FieldYearlyRainIn WeatherUpdateField = "YearlyRainIn"
FieldTotalRainIn WeatherUpdateField = "TotalRainIn"
FieldBatteries WeatherUpdateField = "Batteries"
FieldBaromRelativeIn WeatherUpdateField = "BaromRelativeIn"
FieldBaromAbsoluteIn WeatherUpdateField = "BaromAbsoluteIn"
FieldDewPointF WeatherUpdateField = "DewPointF"
FieldWindChillF WeatherUpdateField = "WindChillF"
)

8
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