From 19823ea08f147c0f81adf0e0713922a8df9782eb Mon Sep 17 00:00:00 2001 From: Ryan D McGuire Date: Sun, 12 Jan 2025 17:23:32 -0500 Subject: [PATCH] Weather service proxy support --- go.mod | 1 + go.sum | 2 + main.go | 5 +- pkg/ambient/ambient.go | 19 ++---- pkg/ambient/{ => config}/config.go | 30 +++------ pkg/provider/awn/provider.go | 88 ++------------------------- pkg/provider/awn/proxy.go | 83 +++++++++++++++++++++++++ pkg/provider/wunderground/provider.go | 66 ++++++-------------- pkg/provider/wunderground/proxy.go | 81 ++++++++++++++++++++++++ pkg/weather/enrich.go | 4 +- pkg/weather/metrics.go | 56 ++++++++--------- pkg/weather/metrics_record.go | 24 ++++---- pkg/weather/types.go | 81 ++++++++++-------------- 13 files changed, 285 insertions(+), 255 deletions(-) rename pkg/ambient/{ => config}/config.go (58%) create mode 100644 pkg/provider/awn/proxy.go create mode 100644 pkg/provider/wunderground/proxy.go diff --git a/go.mod b/go.mod index 5c354f8..951ead5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( go.opentelemetry.io/otel v1.33.0 go.opentelemetry.io/otel/metric v1.33.0 golang.org/x/sys v0.29.0 + k8s.io/utils v0.0.0-20241210054802-24370beab758 ) require ( diff --git a/go.sum b/go.sum index 8a6345b..ec31abf 100644 --- a/go.sum +++ b/go.sum @@ -120,3 +120,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN 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/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= diff --git a/main.go b/main.go index dc7d5c0..59280bb 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "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/config" ) const defaultMetricPrefix = "weather" @@ -19,9 +20,9 @@ func main() { defer cncl() // Config type for app, which implements go-app/config.AppConfig - awConfig := &ambient.AmbientLocalExporterConfig{ + awConfig := &config.AmbientLocalExporterConfig{ MetricPrefix: defaultMetricPrefix, - WeatherStations: make([]ambient.WeatherStation, 0), + WeatherStations: make([]config.WeatherStation, 0), } // Read config and environment, set up logging, load up diff --git a/pkg/ambient/ambient.go b/pkg/ambient/ambient.go index a4fd114..79ae733 100644 --- a/pkg/ambient/ambient.go +++ b/pkg/ambient/ambient.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel/attribute" "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/awn" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/wunderground" @@ -24,7 +25,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 + config *config.AmbientLocalExporterConfig awnProvider provider.AmbientProvider wuProvider provider.AmbientProvider appCtx context.Context @@ -32,7 +33,7 @@ type AmbientWeather struct { l *zerolog.Logger } -func New(appCtx context.Context, awConfig *AmbientLocalExporterConfig) *AmbientWeather { +func New(appCtx context.Context, awConfig *config.AmbientLocalExporterConfig) *AmbientWeather { return &AmbientWeather{ config: awConfig, appCtx: appCtx, @@ -118,7 +119,7 @@ func (aw *AmbientWeather) handleProviderRequest( // 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 := aw.config.GetStation(update.GetStationName()); station != nil { + if station := update.StationConfig; station != nil { if station.ProxyToAWN { err := aw.awnProvider.ProxyReq(ctx, update) if err != nil { @@ -126,14 +127,12 @@ func (aw *AmbientWeather) handleProviderRequest( } } if station.ProxyToWunderground { - err := aw.awnProvider.ProxyReq(ctx, update) + err := aw.wuProvider.ProxyReq(ctx, update) if err != nil { zerolog.Ctx(aw.appCtx).Err(err).Msg("failed to proxy to ambient weather") } } } - - return } func (aw *AmbientWeather) InitMetrics() { @@ -147,13 +146,7 @@ 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.StationInfo = &weather.StationInfo{ - Type: update.StationType, - Equipment: &station.Equipment, - Name: &station.Name, - Keep: station.KeepMetrics, - Drop: station.DropMetrics, - } + update.StationConfig = &station } } } diff --git a/pkg/ambient/config.go b/pkg/ambient/config/config.go similarity index 58% rename from pkg/ambient/config.go rename to pkg/ambient/config/config.go index b706771..4a2c372 100644 --- a/pkg/ambient/config.go +++ b/pkg/ambient/config/config.go @@ -1,9 +1,7 @@ -package ambient +package config 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 @@ -17,11 +15,10 @@ 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"` + // 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"` @@ -31,16 +28,9 @@ type WeatherStation struct { // will be excluded if present in discardMetrics // // If anything is present in keepMetrics, it is solely applied, - // ignoring discardMetrics - KeepMetrics []weather.WeatherUpdateField `yaml:"keepMetrics"` - DropMetrics []weather.WeatherUpdateField `yaml:"dropMetrics"` -} - -func (wc *AmbientLocalExporterConfig) GetStation(name string) *WeatherStation { - for _, station := range wc.WeatherStations { - if station.Name == name { - return &station - } - } - return nil + // ignoring discardMetrics. + // + // Check weather.WeatherUpdateField for options + KeepMetrics []string `yaml:"keepMetrics"` + DropMetrics []string `yaml:"dropMetrics"` } diff --git a/pkg/provider/awn/provider.go b/pkg/provider/awn/provider.go index 80f90ec..1c8cfa2 100644 --- a/pkg/provider/awn/provider.go +++ b/pkg/provider/awn/provider.go @@ -2,17 +2,11 @@ package awn import ( "context" - "errors" "net/http" "net/url" "time" - "github.com/go-resty/resty/v2" "github.com/gorilla/schema" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - - "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/weather" ) @@ -41,74 +35,6 @@ func (awn *AWNProvider) ReqToWeather(_ context.Context, r *http.Request) ( return MapAwnUpdate(awnUpdate), nil } -// 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.WeatherServiceCredentials["PASSKEY"] == "" { - 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.SetStatus(codes.Error, err.Error()) - } - - span.SetAttributes( - attribute.Int("statusCode", resp.StatusCode()), - attribute.String("body", string(resp.Body())), - ) - - return err -} - -func updateToAWNParams(update *weather.WeatherUpdate) *url.Values { - params := &url.Values{} - params.Set("PASSKEY", update.WeatherServiceCredentials["PASSKEY"]) - params.Set("dateutc", time.Now().Format(time.DateTime)) - 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) - return params -} - func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { updateTime := time.Now() if awnUpdate.DateUTC != nil { @@ -118,11 +44,6 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { } } - credentials := make(map[string]string) - if awnUpdate.PassKey != nil { - credentials["PASSKEY"] = *awnUpdate.PassKey - } - return &weather.WeatherUpdate{ DateUTC: &updateTime, StationID: awnUpdate.PassKey, @@ -161,11 +82,10 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { Status: awnUpdate.BattCO2, }, }, - TempIndoorF: awnUpdate.TempInF, - HumidityIndoor: awnUpdate.HumidityIn, - BaromRelativeIn: awnUpdate.BaromRelIn, - BaromAbsoluteIn: awnUpdate.BaromAbsIn, - WeatherServiceCredentials: credentials, + TempIndoorF: awnUpdate.TempInF, + HumidityIndoor: awnUpdate.HumidityIn, + BaromRelativeIn: awnUpdate.BaromRelIn, + BaromAbsoluteIn: awnUpdate.BaromAbsIn, } } diff --git a/pkg/provider/awn/proxy.go b/pkg/provider/awn/proxy.go new file mode 100644 index 0000000..0a1b1e2 --- /dev/null +++ b/pkg/provider/awn/proxy.go @@ -0,0 +1,83 @@ +package awn + +import ( + "context" + "errors" + "net/url" + "time" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + "github.com/go-resty/resty/v2" + "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.SetStatus(codes.Error, err.Error()) + } + + span.SetAttributes( + attribute.Int("statusCode", resp.StatusCode()), + attribute.String("body", string(resp.Body())), + ) + + 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) + return params +} diff --git a/pkg/provider/wunderground/provider.go b/pkg/provider/wunderground/provider.go index 837107b..f33b629 100644 --- a/pkg/provider/wunderground/provider.go +++ b/pkg/provider/wunderground/provider.go @@ -35,29 +35,6 @@ func (wu *WUProvider) ReqToWeather(_ context.Context, r *http.Request) ( return MapWUUpdate(wuUpdate), nil } -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() - // - // resp, err := resty.New().R(). - // SetContext(ctx). - // SetQueryParamsFromValues(r.URL.Query()). - // Get(wuURL) - // if err != nil { - // span.SetStatus(codes.Error, err.Error()) - // span.RecordError(err) - // } - // - // span.SetAttributes( - // attribute.Int("statusCode", resp.StatusCode()), - // attribute.String("body", string(resp.Body())), - // ) - // - // return err - return nil -} - func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate { updateTime := time.Now() @@ -68,32 +45,25 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate { } } - credentials := make(map[string]string) - if wuUpdate.ID != nil && wuUpdate.Password != nil { - credentials["ID"] = *wuUpdate.ID - credentials["Password"] = *wuUpdate.Password - } - return &weather.WeatherUpdate{ - DateUTC: &updateTime, - StationID: wuUpdate.ID, - StationType: wuUpdate.SoftwareType, - TempOutdoorF: wuUpdate.Tempf, - HumidityOudoor: wuUpdate.Humidity, - WindSpeedMPH: wuUpdate.WindGustMPH, - WindGustMPH: wuUpdate.WindGustMPH, - WindDir: wuUpdate.WindDir, - UV: wuUpdate.UV, - SolarRadiation: wuUpdate.SolarRadiation, - HourlyRainIn: wuUpdate.RainIn, - DailyRainIn: wuUpdate.DailyRainIn, - WeeklyRainIn: wuUpdate.WeeklyRainIn, - MonthlyRainIn: wuUpdate.MonthlyRainIn, - YearlyRainIn: wuUpdate.YearlyRainIn, - TempIndoorF: wuUpdate.IndoorTempF, - HumidityIndoor: wuUpdate.IndoorHumidity, - BaromRelativeIn: wuUpdate.BaromIn, - WeatherServiceCredentials: credentials, + DateUTC: &updateTime, + StationID: wuUpdate.ID, + StationType: wuUpdate.SoftwareType, + TempOutdoorF: wuUpdate.Tempf, + HumidityOudoor: wuUpdate.Humidity, + WindSpeedMPH: wuUpdate.WindGustMPH, + WindGustMPH: wuUpdate.WindGustMPH, + WindDir: wuUpdate.WindDir, + UV: wuUpdate.UV, + SolarRadiation: wuUpdate.SolarRadiation, + HourlyRainIn: wuUpdate.RainIn, + DailyRainIn: wuUpdate.DailyRainIn, + WeeklyRainIn: wuUpdate.WeeklyRainIn, + MonthlyRainIn: wuUpdate.MonthlyRainIn, + YearlyRainIn: wuUpdate.YearlyRainIn, + TempIndoorF: wuUpdate.IndoorTempF, + HumidityIndoor: wuUpdate.IndoorHumidity, + BaromRelativeIn: wuUpdate.BaromIn, } } diff --git a/pkg/provider/wunderground/proxy.go b/pkg/provider/wunderground/proxy.go new file mode 100644 index 0000000..0bce744 --- /dev/null +++ b/pkg/provider/wunderground/proxy.go @@ -0,0 +1,81 @@ +package wunderground + +import ( + "context" + "errors" + "net/url" + "time" + + "github.com/go-resty/resty/v2" + "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.RecordError(err) + } + + span.SetAttributes( + attribute.Int("statusCode", resp.StatusCode()), + attribute.String("body", string(resp.Body())), + ) + + return err +} + +func updateToWuParams(u *weather.WeatherUpdate) *url.Values { + params := &url.Values{} + params.Set("dateutc", time.Now().Format(time.DateTime)) + weather.SetURLVal(params, "ID", &u.StationConfig.WundergroundID) + weather.SetURLVal(params, "PASSWORD", &u.StationConfig.WundergroundPassword) + weather.SetURLVal(params, "UV", u.UV) + weather.SetURLVal(params, "action", ptr.To("updateraw")) + 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 +} diff --git a/pkg/weather/enrich.go b/pkg/weather/enrich.go index 03ef95e..fbe54e8 100644 --- a/pkg/weather/enrich.go +++ b/pkg/weather/enrich.go @@ -4,11 +4,13 @@ import ( "math" "net/url" "strconv" + + "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config" ) // Attempts to complete missing fields that may not // be set by a specific provider, such as DewPoint and WindChill -func (u *WeatherUpdate) Enrich() { +func (u *WeatherUpdate) Enrich(weatherStations ...*config.WeatherStation) { if u == nil { return } diff --git a/pkg/weather/metrics.go b/pkg/weather/metrics.go index 69b9c49..d1c4b61 100644 --- a/pkg/weather/metrics.go +++ b/pkg/weather/metrics.go @@ -119,46 +119,46 @@ func (wm *WeatherMetrics) Update(u *WeatherUpdate) { attributes = append(attributes, attribute.String("station_type", *u.StationType)) } - if u.StationInfo != nil { - if u.StationInfo.Name != nil { + if u.StationConfig != nil { + if u.StationConfig.Name != "" { attributes = append(attributes, - attribute.String("station_name", *u.StationInfo.Name)) + attribute.String("station_name", u.StationConfig.Name)) } - if u.StationInfo.Equipment != nil { + if u.StationConfig.Equipment != "" { attributes = append(attributes, - attribute.String("station_equipment", *u.StationInfo.Equipment)) + attribute.String("station_equipment", u.StationConfig.Equipment)) } } - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TempOutdoorF, FloatVal: u.TempOutdoorF, Field: FieldTempOutdoorF, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TempIndoorF, FloatVal: u.TempIndoorF, Field: FieldTempIndoorF, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Int64Gauge: wm.HumidityOudoor, IntVal: u.HumidityOudoor, Field: FieldHumidityOudoor, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Int64Gauge: wm.HumidityIndoor, IntVal: u.HumidityIndoor, Field: FieldHumidityIndoor, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindSpeedMPH, FloatVal: u.WindSpeedMPH, Field: FieldWindSpeedMPH, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindGustMPH, FloatVal: u.WindGustMPH, Field: FieldWindGustMPH, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.MaxDailyGust, FloatVal: u.MaxDailyGust, Field: FieldMaxDailyGust, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Int64Gauge: wm.WindDir, IntVal: u.WindDir, Field: FieldWindDir, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Int64Gauge: wm.WindDirAvg10m, IntVal: u.WindDirAvg10m, Field: FieldWindDirAvg10m, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Int64Gauge: wm.UV, IntVal: u.UV, Field: FieldUV, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.SolarRadiation, FloatVal: u.SolarRadiation, Field: FieldSolarRadiation, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.HourlyRainIn, FloatVal: u.HourlyRainIn, Field: FieldHourlyRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.EventRainIn, FloatVal: u.EventRainIn, Field: FieldEventRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.DailyRainIn, FloatVal: u.DailyRainIn, Field: FieldDailyRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WeeklyRainIn, FloatVal: u.WeeklyRainIn, Field: FieldWeeklyRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.MonthlyRainIn, FloatVal: u.MonthlyRainIn, Field: FieldMonthlyRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.YearlyRainIn, FloatVal: u.YearlyRainIn, Field: FieldYearlyRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TotalRainIn, FloatVal: u.TotalRainIn, Field: FieldTotalRainIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.BaromRelativeIn, FloatVal: u.BaromRelativeIn, Field: FieldBaromRelativeIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.BaromAbsoluteIn, FloatVal: u.BaromAbsoluteIn, Field: FieldBaromAbsoluteIn, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.DewPointF, FloatVal: u.DewPointF, Field: FieldDewPointF, Attributes: attributes, StationInfo: u.StationInfo}) - wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindChillF, FloatVal: u.WindChillF, Field: FieldWindChillF, Attributes: attributes, StationInfo: u.StationInfo}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TempOutdoorF, FloatVal: u.TempOutdoorF, Field: FieldTempOutdoorF, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TempIndoorF, FloatVal: u.TempIndoorF, Field: FieldTempIndoorF, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Int64Gauge: wm.HumidityOudoor, IntVal: u.HumidityOudoor, Field: FieldHumidityOudoor, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Int64Gauge: wm.HumidityIndoor, IntVal: u.HumidityIndoor, Field: FieldHumidityIndoor, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindSpeedMPH, FloatVal: u.WindSpeedMPH, Field: FieldWindSpeedMPH, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindGustMPH, FloatVal: u.WindGustMPH, Field: FieldWindGustMPH, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.MaxDailyGust, FloatVal: u.MaxDailyGust, Field: FieldMaxDailyGust, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Int64Gauge: wm.WindDir, IntVal: u.WindDir, Field: FieldWindDir, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Int64Gauge: wm.WindDirAvg10m, IntVal: u.WindDirAvg10m, Field: FieldWindDirAvg10m, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Int64Gauge: wm.UV, IntVal: u.UV, Field: FieldUV, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.SolarRadiation, FloatVal: u.SolarRadiation, Field: FieldSolarRadiation, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.HourlyRainIn, FloatVal: u.HourlyRainIn, Field: FieldHourlyRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.EventRainIn, FloatVal: u.EventRainIn, Field: FieldEventRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.DailyRainIn, FloatVal: u.DailyRainIn, Field: FieldDailyRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WeeklyRainIn, FloatVal: u.WeeklyRainIn, Field: FieldWeeklyRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.MonthlyRainIn, FloatVal: u.MonthlyRainIn, Field: FieldMonthlyRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.YearlyRainIn, FloatVal: u.YearlyRainIn, Field: FieldYearlyRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.TotalRainIn, FloatVal: u.TotalRainIn, Field: FieldTotalRainIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.BaromRelativeIn, FloatVal: u.BaromRelativeIn, Field: FieldBaromRelativeIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.BaromAbsoluteIn, FloatVal: u.BaromAbsoluteIn, Field: FieldBaromAbsoluteIn, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.DewPointF, FloatVal: u.DewPointF, Field: FieldDewPointF, Attributes: attributes, Station: u.StationConfig}) + wm.recorder.Record(&RecordOpts{Float64Gauge: wm.WindChillF, FloatVal: u.WindChillF, Field: FieldWindChillF, Attributes: attributes, Station: u.StationConfig}) // Batteries 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, StationInfo: u.StationInfo}) + wm.recorder.Record(&RecordOpts{Int64Gauge: wm.BatteryStatus, IntVal: battery.Status, Field: FieldBatteries, Attributes: batAttr, Station: u.StationConfig}) } wm.UpdatesReceived.Add(wm.appCtx, 1) diff --git a/pkg/weather/metrics_record.go b/pkg/weather/metrics_record.go index bbd2f00..b2005d7 100644 --- a/pkg/weather/metrics_record.go +++ b/pkg/weather/metrics_record.go @@ -7,6 +7,8 @@ import ( "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 { @@ -20,15 +22,15 @@ type RecordOpts struct { IntVal *int FloatVal *float64 Attributes []attribute.KeyValue - Field WeatherUpdateField - StationInfo *StationInfo + Field string + Station *config.WeatherStation } func (r *MetricRecorder) Record(opts *RecordOpts) { - if opts.StationInfo != nil && !opts.keep() { + if opts.Station != nil && !opts.keep() { r.l.Trace(). Str("field", string(opts.Field)). - Str("station", *opts.StationInfo.Name). + Str("station", opts.Station.Name). Msg("Metric dropped by station config") return } else if opts.Int64Gauge == nil && opts.Float64Gauge == nil { @@ -39,8 +41,8 @@ func (r *MetricRecorder) Record(opts *RecordOpts) { if opts.Int64Gauge != nil { if opts.IntVal == nil { log := r.l.Trace().Str("field", string(opts.Field)) - if opts.StationInfo != nil { - log = log.Str("station", *opts.StationInfo.Name) + if opts.Station != nil { + log = log.Str("station", opts.Station.Name) } log.Msg("Dropping nil int metric") return @@ -49,8 +51,8 @@ func (r *MetricRecorder) Record(opts *RecordOpts) { } else if opts.Float64Gauge != nil { if opts.FloatVal == nil { log := r.l.Trace().Str("field", string(opts.Field)) - if opts.StationInfo != nil { - log = log.Str("station", *opts.StationInfo.Name) + if opts.Station != nil { + log = log.Str("station", opts.Station.Name) } log.Msg("Dropping nil float metric") return @@ -61,8 +63,8 @@ func (r *MetricRecorder) Record(opts *RecordOpts) { func (o *RecordOpts) keep() bool { // If keep fields are given, only check keep fields - if len(o.StationInfo.Keep) > 0 { - for _, f := range o.StationInfo.Keep { + if len(o.Station.KeepMetrics) > 0 { + for _, f := range o.Station.KeepMetrics { if f == o.Field { return true } @@ -70,7 +72,7 @@ func (o *RecordOpts) keep() bool { return false } - for _, f := range o.StationInfo.Drop { + for _, f := range o.Station.DropMetrics { if f == o.Field { return false } diff --git a/pkg/weather/types.go b/pkg/weather/types.go index dc4cc9c..b7b97f6 100644 --- a/pkg/weather/types.go +++ b/pkg/weather/types.go @@ -2,13 +2,15 @@ package weather import ( "time" + + "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config" ) // Stable intermediate struct containing superset of fields // between AWN and Wunderground style updates from Ambient devices type WeatherUpdate struct { DateUTC *time.Time - StationInfo *StationInfo + StationConfig *config.WeatherStation StationID *string StationType *string TempOutdoorF *float64 @@ -38,15 +40,6 @@ type WeatherUpdate struct { WindChillF *float64 // First URL parameters given to AWN/Wunderground // if proxying is enabled - WeatherServiceCredentials map[string]string -} - -type StationInfo struct { - Type *string - Equipment *string - Name *string - Keep []WeatherUpdateField - Drop []WeatherUpdateField } type BatteryStatus struct { @@ -54,48 +47,40 @@ type BatteryStatus struct { Status *int } -type WeatherUpdateField string - -// NOTE: Annoyance to avoid string constant comparisons -// CHORE: Maintain this +// CHORE: Maintain this, used to check against +// keep and drop lists +// TODO: Use refelct/ast to generate code 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" + 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.StationInfo != nil { - return u.StationInfo.GetName() - } - return "" -} - -func (si *StationInfo) GetName() string { - if si.Name != nil { - return *si.Name + if u.StationConfig != nil { + return u.StationConfig.Name } return "" }