Weather service proxy support
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build and Publish / release (push) Successful in 3m51s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build and Publish / release (push) Successful in 3m51s
				
			This commit is contained in:
		
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								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 ( | ||||
|   | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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= | ||||
|   | ||||
							
								
								
									
										5
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								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 | ||||
|   | ||||
| @@ -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 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -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,10 +15,9 @@ 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 | ||||
| 	// 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 | ||||
| @@ -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"` | ||||
| } | ||||
| @@ -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, | ||||
| @@ -165,7 +86,6 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { | ||||
| 		HumidityIndoor:  awnUpdate.HumidityIn, | ||||
| 		BaromRelativeIn: awnUpdate.BaromRelIn, | ||||
| 		BaromAbsoluteIn: awnUpdate.BaromAbsIn, | ||||
| 		WeatherServiceCredentials: credentials, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										83
									
								
								pkg/provider/awn/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								pkg/provider/awn/proxy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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,12 +45,6 @@ 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, | ||||
| @@ -93,7 +64,6 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate { | ||||
| 		TempIndoorF:     wuUpdate.IndoorTempF, | ||||
| 		HumidityIndoor:  wuUpdate.IndoorHumidity, | ||||
| 		BaromRelativeIn: wuUpdate.BaromIn, | ||||
| 		WeatherServiceCredentials: credentials, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										81
									
								
								pkg/provider/wunderground/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								pkg/provider/wunderground/proxy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
| 		} | ||||
|   | ||||
| @@ -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 "" | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user