Metric implementation
This commit is contained in:
		
							
								
								
									
										9
									
								
								TODO.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								TODO.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # TODO | ||||
|  | ||||
| - [ ] Fix shutdown | ||||
| - [ ] Configuration for app | ||||
| - [ ] Makefile | ||||
| - [ ] Dockerfile | ||||
| - [ ] Helm Chart | ||||
| - [ ] Gitea CI | ||||
| - [ ] Update README | ||||
							
								
								
									
										21
									
								
								config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package main | ||||
|  | ||||
| import "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" | ||||
|  | ||||
| // This configuration extends on go-app config.AppConfig, | ||||
| // setting configuration for the exporter itself | ||||
| type AmbientLocalExporterConfig struct { | ||||
| 	Redis *RedisConfig | ||||
| 	*config.AppConfig | ||||
| } | ||||
|  | ||||
| type RedisConfig struct { | ||||
| 	Enabled    bool   `yaml:"enabled" env:"APP_REDIS_ENABLED" envDefault:"false"` | ||||
| 	Addr       string `yaml:"addr" env:"APP_REDIS_ADDR"` | ||||
| 	ClientName string `yaml:"clientName" env:"APP_REDIS_CLIENT_NAME"` | ||||
| 	Protocol   int    `yaml:"protocol" env:"APP_REDIS_PROTOCOL"` | ||||
| 	Username   string `yaml:"username" env:"APP_REDIS_USERNAME"` | ||||
| 	Password   string `yaml:"password" env:"APP_REDIS_PASSWORD"` | ||||
| 	DB         int    `yaml:"db" env:"APP_REDIS_DB"` | ||||
| 	MaxRetries int    `yaml:"maxRetries" env:"APP_REDIS_MAX_RETRIES"` | ||||
| } | ||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,10 +3,11 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter | ||||
| go 1.23.4 | ||||
|  | ||||
| require ( | ||||
| 	gitea.libretechconsulting.com/rmcguire/go-app v0.1.0 | ||||
| 	gitea.libretechconsulting.com/rmcguire/go-app v0.1.1 | ||||
| 	github.com/gorilla/schema v1.4.1 | ||||
| 	github.com/rs/zerolog v1.33.0 | ||||
| 	go.opentelemetry.io/otel v1.33.0 | ||||
| 	go.opentelemetry.io/otel/metric v1.33.0 | ||||
| 	golang.org/x/sys v0.29.0 | ||||
| ) | ||||
|  | ||||
| @@ -36,7 +37,6 @@ require ( | ||||
| 	go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect | ||||
| 	go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect | ||||
| 	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect | ||||
| 	go.opentelemetry.io/otel/metric v1.33.0 // indirect | ||||
| 	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 | ||||
|   | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| 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= | ||||
| 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= | ||||
|   | ||||
							
								
								
									
										7
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								main.go
									
									
									
									
									
								
							| @@ -18,17 +18,20 @@ func main() { | ||||
|  | ||||
| 	ctx = app.MustSetupConfigAndLogging(ctx) | ||||
|  | ||||
| 	aw := ambient.New(ctx) | ||||
| 	aw.MustInit() | ||||
|  | ||||
| 	awApp := app.App{ | ||||
| 		AppContext: ctx, | ||||
| 		HTTP: &app.AppHTTP{ | ||||
| 			Funcs: []srv.HTTPFunc{ | ||||
| 				{ | ||||
| 					Path:        "/weatherstation/updateweatherstation.php", | ||||
| 					HandlerFunc: ambient.GetWundergroundHandlerFunc(ctx), | ||||
| 					HandlerFunc: aw.GetWundergroundHandlerFunc(ctx), | ||||
| 				}, | ||||
| 				{ | ||||
| 					Path:        "/data/report", | ||||
| 					HandlerFunc: ambient.GetAWNHandlerFunc(ctx), | ||||
| 					HandlerFunc: aw.GetAWNHandlerFunc(ctx), | ||||
| 				}, | ||||
| 			}, | ||||
| 			HealthChecks: []srv.HealthCheckFunc{ | ||||
|   | ||||
| @@ -16,26 +16,42 @@ import ( | ||||
| 	"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" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/weather" | ||||
| ) | ||||
|  | ||||
| // These providers implement support for the update sent | ||||
| // when either "AmbientWeather" or "Wunderground" are selected | ||||
| // in the "Custom" section of the AWNet app, or the web UI | ||||
| // of an Ambient WeatherHub | ||||
| var ( | ||||
| 	awnProvider = &awn.AWNProvider{} | ||||
| 	wuProvider  = &wunderground.WUProvider{} | ||||
| ) | ||||
| type AmbientWeather struct { | ||||
| 	// These providers implement support for the update sent | ||||
| 	// when either "AmbientWeather" or "Wunderground" are selected | ||||
| 	// in the "Custom" section of the AWNet app, or the web UI | ||||
| 	// of an Ambient WeatherHub | ||||
| 	awnProvider provider.AmbientProvider | ||||
| 	wuProvider  provider.AmbientProvider | ||||
| 	appCtx      context.Context | ||||
| 	metrics     *weather.WeatherMetrics | ||||
| 	l           *zerolog.Logger | ||||
| } | ||||
|  | ||||
| func GetAWNHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		handleProviderRequest(appCtx, awnProvider, w, r) | ||||
| func New(appCtx context.Context) *AmbientWeather { | ||||
| 	return &AmbientWeather{ | ||||
| 		appCtx: appCtx, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func GetWundergroundHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) { | ||||
| func (aw *AmbientWeather) MustInit() { | ||||
| 	aw.awnProvider = &awn.AWNProvider{} | ||||
| 	aw.wuProvider = &wunderground.WUProvider{} | ||||
| 	aw.l = zerolog.Ctx(aw.appCtx) | ||||
| } | ||||
|  | ||||
| func (aw *AmbientWeather) GetAWNHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		handleProviderRequest(appCtx, wuProvider, w, r) | ||||
| 		aw.handleProviderRequest(aw.awnProvider, w, r) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (aw *AmbientWeather) GetWundergroundHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		aw.handleProviderRequest(aw.wuProvider, w, r) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -43,14 +59,13 @@ func GetWundergroundHandlerFunc(appCtx context.Context) func(http.ResponseWriter | ||||
| // 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 | ||||
| func handleProviderRequest( | ||||
| 	appCtx context.Context, | ||||
| func (aw *AmbientWeather) handleProviderRequest( | ||||
| 	p provider.AmbientProvider, | ||||
| 	w http.ResponseWriter, | ||||
| 	r *http.Request, | ||||
| ) { | ||||
| 	l := zerolog.Ctx(appCtx) | ||||
| 	tracer := otel.GetTracer(appCtx, p.Name()+".http.handler") | ||||
| 	l := zerolog.Ctx(aw.appCtx) | ||||
| 	tracer := otel.GetTracer(aw.appCtx, p.Name()+".http.handler") | ||||
|  | ||||
| 	ctx, span := tracer.Start(r.Context(), p.Name()+".update") | ||||
| 	span.SetAttributes(attribute.String("provider", p.Name())) | ||||
| @@ -76,6 +91,12 @@ func handleProviderRequest( | ||||
| 	// such as dew point and wind chill | ||||
| 	update.Enrich() | ||||
|  | ||||
| 	// Update metrics | ||||
| 	if aw.metrics == nil { | ||||
| 		aw.metrics = weather.MustInitMetrics(aw.appCtx) | ||||
| 	} | ||||
| 	aw.metrics.Update(update) | ||||
|  | ||||
| 	l.Debug(). | ||||
| 		Str("provider", p.Name()). | ||||
| 		Any("update", update). | ||||
|   | ||||
| @@ -41,13 +41,13 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { | ||||
| 	return &weather.WeatherUpdate{ | ||||
| 		StationType:       awnUpdate.StationType, | ||||
| 		DateUTC:           &updateTime, | ||||
| 		TempF:             awnUpdate.TempF, | ||||
| 		Humidity:          awnUpdate.Humidity, | ||||
| 		TempOutdoorF:             awnUpdate.TempF, | ||||
| 		HumidityOudoor:          awnUpdate.Humidity, | ||||
| 		WindSpeedMPH:      awnUpdate.WindGustMPH, | ||||
| 		WindGustMPH:       awnUpdate.WindGustMPH, | ||||
| 		MaxDailyGust:      awnUpdate.MaxDailyGust, | ||||
| 		WindDir:           awnUpdate.WindDir, | ||||
| 		WindDirAVG10m:     awnUpdate.WindDirAVG10m, | ||||
| 		WindDirAvg10m:     awnUpdate.WindDirAVG10m, | ||||
| 		UV:                awnUpdate.UV, | ||||
| 		SolarRadiation:    awnUpdate.SolarRadiation, | ||||
| 		HourlyRainIn:      awnUpdate.HourlyRainIn, | ||||
| @@ -60,8 +60,8 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate { | ||||
| 		BattOutdoorSensor: awnUpdate.BattOut, | ||||
| 		BattIndoorSensor:  awnUpdate.BattIn, | ||||
| 		BattRainSensor:    awnUpdate.BattRain, | ||||
| 		TempInsideF:       awnUpdate.TempInF, | ||||
| 		HumidityInside:    awnUpdate.HumidityIn, | ||||
| 		TempIndoorF:       awnUpdate.TempInF, | ||||
| 		HumidityIndoor:    awnUpdate.HumidityIn, | ||||
| 		BaromRelativeIn:   awnUpdate.BaromRelIn, | ||||
| 		BaromAbsoluteIn:   awnUpdate.BaromAbsIn, | ||||
| 	} | ||||
|   | ||||
| @@ -4,27 +4,27 @@ type AmbientWeatherUpdate struct { | ||||
| 	PassKey        string  `json:"PASSKEY,omitempty" schema:"PASSKEY"` | ||||
| 	StationType    string  `json:"stationtype,omitempty" schema:"stationtype"` | ||||
| 	DateUTC        string  `json:"dateutc,omitempty" schema:"dateutc"` | ||||
| 	TempF          float32 `json:"tempf,omitempty" schema:"tempf"` | ||||
| 	TempF          float64 `json:"tempf,omitempty" schema:"tempf"` | ||||
| 	Humidity       int     `json:"humidity,omitempty" schema:"humidity"` | ||||
| 	WindSpeedMPH   float32 `json:"windspeedmph,omitempty" schema:"windspeedmph"` | ||||
| 	WindGustMPH    float32 `json:"windgustmph,omitempty" schema:"windgustmph"` | ||||
| 	MaxDailyGust   float32 `json:"maxdailygust,omitempty" schema:"maxdailygust"` | ||||
| 	WindSpeedMPH   float64 `json:"windspeedmph,omitempty" schema:"windspeedmph"` | ||||
| 	WindGustMPH    float64 `json:"windgustmph,omitempty" schema:"windgustmph"` | ||||
| 	MaxDailyGust   float64 `json:"maxdailygust,omitempty" schema:"maxdailygust"` | ||||
| 	WindDir        int     `json:"winddir,omitempty" schema:"winddir"` | ||||
| 	WindDirAVG10m  int     `json:"winddir_avg10m,omitempty" schema:"winddir_avg10m"` | ||||
| 	UV             int     `json:"uv,omitempty" schema:"uv"` | ||||
| 	SolarRadiation float32 `json:"solarradiation,omitempty" schema:"solarradiation"` | ||||
| 	HourlyRainIn   float32 `json:"hourlyrainin,omitempty" schema:"hourlyrainin"` | ||||
| 	EventRainIn    float32 `json:"eventrainin,omitempty" schema:"eventrainin"` | ||||
| 	DailyRainIn    float32 `json:"dailyrainin,omitempty" schema:"dailyrainin"` | ||||
| 	WeeklyRainIn   float32 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"` | ||||
| 	MonthlyRainIn  float32 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"` | ||||
| 	YearlyRainIn   float32 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"` | ||||
| 	TotalRainIn    float32 `json:"totalrainin,omitempty" schema:"totalrainin"` | ||||
| 	SolarRadiation float64 `json:"solarradiation,omitempty" schema:"solarradiation"` | ||||
| 	HourlyRainIn   float64 `json:"hourlyrainin,omitempty" schema:"hourlyrainin"` | ||||
| 	EventRainIn    float64 `json:"eventrainin,omitempty" schema:"eventrainin"` | ||||
| 	DailyRainIn    float64 `json:"dailyrainin,omitempty" schema:"dailyrainin"` | ||||
| 	WeeklyRainIn   float64 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"` | ||||
| 	MonthlyRainIn  float64 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"` | ||||
| 	YearlyRainIn   float64 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"` | ||||
| 	TotalRainIn    float64 `json:"totalrainin,omitempty" schema:"totalrainin"` | ||||
| 	BattOut        int     `json:"battout,omitempty" schema:"battout"` | ||||
| 	BattRain       int     `json:"battrain,omitempty" schema:"battrain"` | ||||
| 	TempInF        float32 `json:"tempinf,omitempty" schema:"tempinf"` | ||||
| 	TempInF        float64 `json:"tempinf,omitempty" schema:"tempinf"` | ||||
| 	HumidityIn     int     `json:"humidityin,omitempty" schema:"humidityin"` | ||||
| 	BaromRelIn     float32 `json:"baromrelin,omitempty" schema:"baromrelin"` | ||||
| 	BaromAbsIn     float32 `json:"baromabsin,omitempty" schema:"baromabsin"` | ||||
| 	BaromRelIn     float64 `json:"baromrelin,omitempty" schema:"baromrelin"` | ||||
| 	BaromAbsIn     float64 `json:"baromabsin,omitempty" schema:"baromabsin"` | ||||
| 	BattIn         int     `json:"battin,omitempty" schema:"battin"` | ||||
| } | ||||
|   | ||||
| @@ -41,8 +41,8 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate { | ||||
| 	return &weather.WeatherUpdate{ | ||||
| 		StationType:     wuUpdate.SoftwareType, | ||||
| 		DateUTC:         &updateTime, | ||||
| 		TempF:           wuUpdate.Tempf, | ||||
| 		Humidity:        wuUpdate.Humidity, | ||||
| 		TempOutdoorF:           wuUpdate.Tempf, | ||||
| 		HumidityOudoor:        wuUpdate.Humidity, | ||||
| 		WindSpeedMPH:    wuUpdate.WindGustMPH, | ||||
| 		WindGustMPH:     wuUpdate.WindGustMPH, | ||||
| 		WindDir:         wuUpdate.WindDir, | ||||
| @@ -53,8 +53,8 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate { | ||||
| 		WeeklyRainIn:    wuUpdate.WeeklyRainIn, | ||||
| 		MonthlyRainIn:   wuUpdate.MonthlyRainIn, | ||||
| 		YearlyRainIn:    wuUpdate.YearlyRainIn, | ||||
| 		TempInsideF:     wuUpdate.IndoorTempF, | ||||
| 		HumidityInside:  wuUpdate.IndoorHumidity, | ||||
| 		TempIndoorF:     wuUpdate.IndoorTempF, | ||||
| 		HumidityIndoor:  wuUpdate.IndoorHumidity, | ||||
| 		BaromRelativeIn: wuUpdate.BaromIn, | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,25 +5,25 @@ type WundergroundUpdate struct { | ||||
| 	Password       string  `json:"PASSWORD,omitempty" schema:"PASSWORD"` | ||||
| 	UV             int     `json:"UV,omitempty" schema:"UV"` | ||||
| 	Action         string  `json:"action,omitempty" schema:"action"` | ||||
| 	BaromIn        float32 `json:"baromin,omitempty" schema:"baromin"` | ||||
| 	DailyRainIn    float32 `json:"dailyrainin,omitempty" schema:"dailyrainin"` | ||||
| 	BaromIn        float64 `json:"baromin,omitempty" schema:"baromin"` | ||||
| 	DailyRainIn    float64 `json:"dailyrainin,omitempty" schema:"dailyrainin"` | ||||
| 	DateUTC        string  `json:"dateutc,omitempty" schema:"dateutc"` | ||||
| 	DewPtF         float32 `json:"dewptf,omitempty" schema:"dewptf"` | ||||
| 	DewPtF         float64 `json:"dewptf,omitempty" schema:"dewptf"` | ||||
| 	Humidity       int     `json:"humidity,omitempty" schema:"humidity"` | ||||
| 	IndoorHumidity int     `json:"indoorhumidity,omitempty" schema:"indoorhumidity"` | ||||
| 	IndoorTempF    float32 `json:"indoortempf,omitempty" schema:"indoortempf"` | ||||
| 	IndoorTempF    float64 `json:"indoortempf,omitempty" schema:"indoortempf"` | ||||
| 	LowBatt        bool    `json:"lowbatt,omitempty" schema:"lowbatt"` | ||||
| 	MonthlyRainIn  float32 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"` | ||||
| 	RainIn         float32 `json:"rainin,omitempty" schema:"rainin"` | ||||
| 	MonthlyRainIn  float64 `json:"monthlyrainin,omitempty" schema:"monthlyrainin"` | ||||
| 	RainIn         float64 `json:"rainin,omitempty" schema:"rainin"` | ||||
| 	Realtime       bool    `json:"realtime,omitempty" schema:"realtime"` | ||||
| 	Rtfreq         int     `json:"rtfreq,omitempty" schema:"rtfreq"` | ||||
| 	SoftwareType   string  `json:"softwaretype,omitempty" schema:"softwaretype"` | ||||
| 	SolarRadiation float32 `json:"solarradiation,omitempty" schema:"solarradiation"` | ||||
| 	Tempf          float32 `json:"tempf,omitempty" schema:"tempf"` | ||||
| 	WeeklyRainIn   float32 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"` | ||||
| 	WindChillF     float32 `json:"windchillf,omitempty" schema:"windchillf"` | ||||
| 	SolarRadiation float64 `json:"solarradiation,omitempty" schema:"solarradiation"` | ||||
| 	Tempf          float64 `json:"tempf,omitempty" schema:"tempf"` | ||||
| 	WeeklyRainIn   float64 `json:"weeklyrainin,omitempty" schema:"weeklyrainin"` | ||||
| 	WindChillF     float64 `json:"windchillf,omitempty" schema:"windchillf"` | ||||
| 	WindDir        int     `json:"winddir,omitempty" schema:"winddir"` | ||||
| 	WindGustMPH    float32 `json:"windgustmph,omitempty" schema:"windgustmph"` | ||||
| 	WindSpeedMPH   float32 `json:"windspeedmph,omitempty" schema:"windspeedmph"` | ||||
| 	YearlyRainIn   float32 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"` | ||||
| 	WindGustMPH    float64 `json:"windgustmph,omitempty" schema:"windgustmph"` | ||||
| 	WindSpeedMPH   float64 `json:"windspeedmph,omitempty" schema:"windspeedmph"` | ||||
| 	YearlyRainIn   float64 `json:"yearlyrainin,omitempty" schema:"yearlyrainin"` | ||||
| } | ||||
|   | ||||
| @@ -4,18 +4,13 @@ import "math" | ||||
|  | ||||
| // Attempts to complete missing fields that may not | ||||
| // be set by a specific provider, such as DewPoint and WindChill | ||||
| // | ||||
| // If enrich is called repeated with the same station ID, measurements | ||||
| // will be recorded to produce averages. This will be more stable | ||||
| // and support scaling if Redis is available | ||||
| // TODO: Implement average tracker | ||||
| func (u *WeatherUpdate) Enrich() { | ||||
| 	if u.WindChillF == 0 { | ||||
| 		u.WindChillF = CalculateWindChill(u.TempF, u.WindSpeedMPH) | ||||
| 		u.WindChillF = CalculateWindChill(u.TempOutdoorF, u.WindSpeedMPH) | ||||
| 	} | ||||
|  | ||||
| 	if u.DewPointF == 0 { | ||||
| 		u.DewPointF = CalculateDewPoint(u.TempF, float32(u.Humidity)) | ||||
| 		u.DewPointF = CalculateDewPoint(u.TempOutdoorF, float64(u.HumidityOudoor)) | ||||
| 	} | ||||
|  | ||||
| 	if u.BaromAbsoluteIn == 0 { | ||||
| @@ -23,15 +18,15 @@ func (u *WeatherUpdate) Enrich() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func CalculateDewPoint(tempF, humidity float32) float32 { | ||||
| func CalculateDewPoint(tempF, humidity float64) float64 { | ||||
| 	// Convert temperature from Fahrenheit to Celsius | ||||
| 	tempC := (tempF - 32) * 5 / 9 | ||||
|  | ||||
| 	// Calculate the dew point using the Magnus-Tetens approximation | ||||
| 	a := float32(17.27) | ||||
| 	b := float32(237.7) | ||||
| 	a := float64(17.27) | ||||
| 	b := float64(237.7) | ||||
|  | ||||
| 	alpha := (a*tempC)/(b+tempC) + float32(math.Log(float64(humidity)/100)) | ||||
| 	alpha := (a*tempC)/(b+tempC) + math.Log(humidity/100) | ||||
| 	dewPointC := (b * alpha) / (a - alpha) | ||||
|  | ||||
| 	// Convert dew point back to Fahrenheit | ||||
| @@ -39,16 +34,14 @@ func CalculateDewPoint(tempF, humidity float32) float32 { | ||||
| 	return dewPointF | ||||
| } | ||||
|  | ||||
| func CalculateWindChill(tempF float32, windSpeedMPH float32) float32 { | ||||
| func CalculateWindChill(tempF float64, windSpeedMPH float64) float64 { | ||||
| 	if windSpeedMPH <= 3 { | ||||
| 		// Wind chill calculation doesn't apply for very low wind speeds | ||||
| 		return tempF | ||||
| 	} | ||||
|  | ||||
| 	// Formula for calculating wind chill | ||||
| 	return float32( | ||||
| 		35.74 + 0.6215*float64(tempF) - | ||||
| 			35.75*math.Pow(float64(windSpeedMPH), 0.16) + | ||||
| 			0.4275*float64(tempF)* | ||||
| 				math.Pow(float64(windSpeedMPH), 0.16)) | ||||
| 	return 35.74 + 0.6215*tempF - | ||||
| 		35.75*math.Pow(windSpeedMPH, 0.16) + | ||||
| 		0.4275*tempF*math.Pow(windSpeedMPH, 0.16) | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,148 @@ | ||||
| package weather | ||||
|  | ||||
| // TODO: Add OTEL Metrics | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/metric" | ||||
| 	semconv "go.opentelemetry.io/otel/semconv/v1.26.0" | ||||
| ) | ||||
|  | ||||
| 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 | ||||
|  | ||||
| 	// Internal Telemetry | ||||
| 	UpdatesReceived metric.Int64Counter | ||||
| 	appCtx          context.Context | ||||
| 	cfg             *config.AppConfig | ||||
| 	meter           metric.Meter | ||||
| } | ||||
|  | ||||
| func MustInitMetrics(appCtx context.Context) *WeatherMetrics { | ||||
| 	wm := &WeatherMetrics{ | ||||
| 		appCtx: appCtx, | ||||
| 		cfg:    config.MustFromCtx(appCtx), | ||||
| 	} | ||||
|  | ||||
| 	wm.meter = otel.GetMeter(appCtx, "weather", "metrics") | ||||
|  | ||||
| 	// Weather Metrics | ||||
| 	wm.TempOutdoorF, _ = wm.meter.Float64Gauge("weather_temp_outdoor_f", | ||||
| 		metric.WithDescription("Outdoor Temperature in Faherenheit")) | ||||
| 	wm.TempIndoorF, _ = wm.meter.Float64Gauge("weather_temp_indoor_f", | ||||
| 		metric.WithDescription("Indoor Temperature in Faherenheit")) | ||||
| 	wm.HumidityOudoor, _ = wm.meter.Int64Gauge("weather_humidity_oudoor", | ||||
| 		metric.WithDescription("Outdoor Humidity %")) | ||||
| 	wm.HumidityIndoor, _ = wm.meter.Int64Gauge("weather_humidity_indoor", | ||||
| 		metric.WithDescription("Indoor Humidity %")) | ||||
| 	wm.WindSpeedMPH, _ = wm.meter.Float64Gauge("weather_wind_speed_mph", | ||||
| 		metric.WithDescription("Wind Speed in MPH")) | ||||
| 	wm.WindGustMPH, _ = wm.meter.Float64Gauge("weather_wind_gust_mph", | ||||
| 		metric.WithDescription("Wind Gust in MPH")) | ||||
| 	wm.MaxDailyGust, _ = wm.meter.Float64Gauge("weather_max_daily_gust", | ||||
| 		metric.WithDescription("Max Daily Wind Gust")) | ||||
| 	wm.WindDir, _ = wm.meter.Int64Gauge("weather_wind_dir", | ||||
| 		metric.WithDescription("Wind Direction in Degrees")) | ||||
| 	wm.WindDirAvg10m, _ = wm.meter.Int64Gauge("weather_wind_dir_avg_10m", | ||||
| 		metric.WithDescription("Wind Direction 10m Average")) | ||||
| 	wm.UV, _ = wm.meter.Int64Gauge("weather_uv", | ||||
| 		metric.WithDescription("UV Index")) | ||||
| 	wm.SolarRadiation, _ = wm.meter.Float64Gauge("weather_solar_radiation", | ||||
| 		metric.WithDescription("Solar Radiation in W/㎡")) | ||||
| 	wm.HourlyRainIn, _ = wm.meter.Float64Gauge("weather_hourly_rain_in", | ||||
| 		metric.WithDescription("Hourly Rain in Inches")) | ||||
| 	wm.EventRainIn, _ = wm.meter.Float64Gauge("weather_event_rain_in", | ||||
| 		metric.WithDescription("Event Rain in Inches")) | ||||
| 	wm.DailyRainIn, _ = wm.meter.Float64Gauge("weather_daily_rain_in", | ||||
| 		metric.WithDescription("Daily Rain in Inches")) | ||||
| 	wm.WeeklyRainIn, _ = wm.meter.Float64Gauge("weather_weekly_rain_in", | ||||
| 		metric.WithDescription("Weekly Rain in Inches")) | ||||
| 	wm.MonthlyRainIn, _ = wm.meter.Float64Gauge("weather_monthly_rain_in", | ||||
| 		metric.WithDescription("Monthly Rain in Inches")) | ||||
| 	wm.YearlyRainIn, _ = wm.meter.Float64Gauge("weather_yearly_rain_in", | ||||
| 		metric.WithDescription("Yearly Rain in Inches")) | ||||
| 	wm.TotalRainIn, _ = wm.meter.Float64Gauge("weather_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", | ||||
| 		metric.WithDescription("Relative Pressure in Inches of Mercury")) | ||||
| 	wm.BaromAbsoluteIn, _ = wm.meter.Float64Gauge("weather_barometric_pressure_absolute_in", | ||||
| 		metric.WithDescription("Absolute Pressure in Inches of Mercury")) | ||||
| 	wm.DewPointF, _ = wm.meter.Float64Gauge("weather_dew_point_f", | ||||
| 		metric.WithDescription("Dew Point in Faherenheit")) | ||||
| 	wm.WindChillF, _ = wm.meter.Float64Gauge("weather_wind_chill_f", | ||||
| 		metric.WithDescription("Wind Chill in Faherenheit")) | ||||
|  | ||||
| 	// Internal Telemetry | ||||
| 	wm.UpdatesReceived, _ = wm.meter.Int64Counter("weather_updates_received", | ||||
| 		metric.WithDescription("Metric Updates Processed by Exporter")) | ||||
|  | ||||
| 	return wm | ||||
| } | ||||
|  | ||||
| func (wm *WeatherMetrics) Update(u *WeatherUpdate) { | ||||
| 	attributes := attribute.NewSet( | ||||
| 		semconv.ServiceVersion(wm.cfg.Version), | ||||
| 		attribute.String("station_type", u.StationType), | ||||
| 	) | ||||
|  | ||||
| 	wm.TempOutdoorF.Record(wm.appCtx, u.TempOutdoorF, metric.WithAttributeSet(attributes)) | ||||
| 	wm.TempIndoorF.Record(wm.appCtx, u.TempIndoorF, metric.WithAttributeSet(attributes)) | ||||
| 	wm.HumidityOudoor.Record(wm.appCtx, int64(u.HumidityOudoor), metric.WithAttributeSet(attributes)) | ||||
| 	wm.HumidityIndoor.Record(wm.appCtx, int64(u.HumidityIndoor), metric.WithAttributeSet(attributes)) | ||||
| 	wm.WindSpeedMPH.Record(wm.appCtx, u.WindSpeedMPH, metric.WithAttributeSet(attributes)) | ||||
| 	wm.WindGustMPH.Record(wm.appCtx, u.WindGustMPH, metric.WithAttributeSet(attributes)) | ||||
| 	wm.MaxDailyGust.Record(wm.appCtx, u.MaxDailyGust, metric.WithAttributeSet(attributes)) | ||||
| 	wm.WindDir.Record(wm.appCtx, int64(u.WindDir), metric.WithAttributeSet(attributes)) | ||||
| 	wm.WindDirAvg10m.Record(wm.appCtx, int64(u.WindDirAvg10m), metric.WithAttributeSet(attributes)) | ||||
| 	wm.UV.Record(wm.appCtx, int64(u.UV), metric.WithAttributeSet(attributes)) | ||||
| 	wm.SolarRadiation.Record(wm.appCtx, u.SolarRadiation, metric.WithAttributeSet(attributes)) | ||||
| 	wm.HourlyRainIn.Record(wm.appCtx, u.HourlyRainIn, metric.WithAttributeSet(attributes)) | ||||
| 	wm.EventRainIn.Record(wm.appCtx, u.EventRainIn, metric.WithAttributeSet(attributes)) | ||||
| 	wm.DailyRainIn.Record(wm.appCtx, u.DailyRainIn, metric.WithAttributeSet(attributes)) | ||||
| 	wm.WeeklyRainIn.Record(wm.appCtx, u.WeeklyRainIn, metric.WithAttributeSet(attributes)) | ||||
| 	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)) | ||||
|  | ||||
| 	wm.UpdatesReceived.Add(wm.appCtx, 1) | ||||
| } | ||||
|   | ||||
| @@ -9,31 +9,31 @@ import ( | ||||
| type WeatherUpdate struct { | ||||
| 	DateUTC           *time.Time | ||||
| 	StationType       string | ||||
| 	TempF             float32 | ||||
| 	TempInsideF       float32 | ||||
| 	Humidity          int | ||||
| 	HumidityInside    int | ||||
| 	WindSpeedMPH      float32 | ||||
| 	WindGustMPH       float32 | ||||
| 	MaxDailyGust      float32 | ||||
| 	TempOutdoorF      float64 | ||||
| 	TempIndoorF       float64 | ||||
| 	HumidityOudoor    int | ||||
| 	HumidityIndoor    int | ||||
| 	WindSpeedMPH      float64 | ||||
| 	WindGustMPH       float64 | ||||
| 	MaxDailyGust      float64 | ||||
| 	WindDir           int | ||||
| 	WindDirAVG10m     int | ||||
| 	WindDirAvg10m     int | ||||
| 	UV                int | ||||
| 	SolarRadiation    float32 | ||||
| 	HourlyRainIn      float32 | ||||
| 	EventRainIn       float32 | ||||
| 	DailyRainIn       float32 | ||||
| 	WeeklyRainIn      float32 | ||||
| 	MonthlyRainIn     float32 | ||||
| 	YearlyRainIn      float32 | ||||
| 	TotalRainIn       float32 | ||||
| 	SolarRadiation    float64 | ||||
| 	HourlyRainIn      float64 | ||||
| 	EventRainIn       float64 | ||||
| 	DailyRainIn       float64 | ||||
| 	WeeklyRainIn      float64 | ||||
| 	MonthlyRainIn     float64 | ||||
| 	YearlyRainIn      float64 | ||||
| 	TotalRainIn       float64 | ||||
| 	BattOutdoorSensor int | ||||
| 	BattIndoorSensor  int | ||||
| 	BattRainSensor    int | ||||
| 	BaromRelativeIn   float32 | ||||
| 	BaromAbsoluteIn   float32 | ||||
| 	BaromRelativeIn   float64 | ||||
| 	BaromAbsoluteIn   float64 | ||||
| 	// These fields may be calculated | ||||
| 	// if not otherwise set | ||||
| 	DewPointF  float32 | ||||
| 	WindChillF float32 | ||||
| 	DewPointF  float64 | ||||
| 	WindChillF float64 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user