Metric implementation

This commit is contained in:
Ryan McGuire 2025-01-04 20:54:36 -05:00
parent 23da46fe07
commit 59b598c6b3
13 changed files with 290 additions and 96 deletions

9
TODO.md Normal file
View File

@ -0,0 +1,9 @@
# TODO
- [ ] Fix shutdown
- [ ] Configuration for app
- [ ] Makefile
- [ ] Dockerfile
- [ ] Helm Chart
- [ ] Gitea CI
- [ ] Update README

21
config.go Normal file
View 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
View File

@ -3,10 +3,11 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter
go 1.23.4 go 1.23.4
require ( require (
gitea.libretechconsulting.com/rmcguire/go-app v0.1.0 gitea.libretechconsulting.com/rmcguire/go-app v0.1.1
github.com/gorilla/schema v1.4.1 github.com/gorilla/schema v1.4.1
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
go.opentelemetry.io/otel v1.33.0 go.opentelemetry.io/otel v1.33.0
go.opentelemetry.io/otel/metric v1.33.0
golang.org/x/sys v0.29.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/prometheus v0.55.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.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/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 v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect

2
go.sum
View File

@ -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 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.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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=

View File

@ -18,17 +18,20 @@ func main() {
ctx = app.MustSetupConfigAndLogging(ctx) ctx = app.MustSetupConfigAndLogging(ctx)
aw := ambient.New(ctx)
aw.MustInit()
awApp := app.App{ awApp := app.App{
AppContext: ctx, AppContext: ctx,
HTTP: &app.AppHTTP{ HTTP: &app.AppHTTP{
Funcs: []srv.HTTPFunc{ Funcs: []srv.HTTPFunc{
{ {
Path: "/weatherstation/updateweatherstation.php", Path: "/weatherstation/updateweatherstation.php",
HandlerFunc: ambient.GetWundergroundHandlerFunc(ctx), HandlerFunc: aw.GetWundergroundHandlerFunc(ctx),
}, },
{ {
Path: "/data/report", Path: "/data/report",
HandlerFunc: ambient.GetAWNHandlerFunc(ctx), HandlerFunc: aw.GetAWNHandlerFunc(ctx),
}, },
}, },
HealthChecks: []srv.HealthCheckFunc{ HealthChecks: []srv.HealthCheckFunc{

View File

@ -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"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/awn" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/awn"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/wunderground" "gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/provider/wunderground"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/weather"
) )
type AmbientWeather struct {
// These providers implement support for the update sent // These providers implement support for the update sent
// when either "AmbientWeather" or "Wunderground" are selected // when either "AmbientWeather" or "Wunderground" are selected
// in the "Custom" section of the AWNet app, or the web UI // in the "Custom" section of the AWNet app, or the web UI
// of an Ambient WeatherHub // of an Ambient WeatherHub
var ( awnProvider provider.AmbientProvider
awnProvider = &awn.AWNProvider{} wuProvider provider.AmbientProvider
wuProvider = &wunderground.WUProvider{} appCtx context.Context
) metrics *weather.WeatherMetrics
l *zerolog.Logger
}
func GetAWNHandlerFunc(appCtx context.Context) func(http.ResponseWriter, *http.Request) { func New(appCtx context.Context) *AmbientWeather {
return func(w http.ResponseWriter, r *http.Request) { return &AmbientWeather{
handleProviderRequest(appCtx, awnProvider, w, r) 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) { 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 // stable type. Enrich is called on the type to complete
// any missing fields as the two providers supported by Ambient // any missing fields as the two providers supported by Ambient
// devices (awn/wunderground) produce different fields // devices (awn/wunderground) produce different fields
func handleProviderRequest( func (aw *AmbientWeather) handleProviderRequest(
appCtx context.Context,
p provider.AmbientProvider, p provider.AmbientProvider,
w http.ResponseWriter, w http.ResponseWriter,
r *http.Request, r *http.Request,
) { ) {
l := zerolog.Ctx(appCtx) l := zerolog.Ctx(aw.appCtx)
tracer := otel.GetTracer(appCtx, p.Name()+".http.handler") tracer := otel.GetTracer(aw.appCtx, p.Name()+".http.handler")
ctx, span := tracer.Start(r.Context(), p.Name()+".update") ctx, span := tracer.Start(r.Context(), p.Name()+".update")
span.SetAttributes(attribute.String("provider", p.Name())) span.SetAttributes(attribute.String("provider", p.Name()))
@ -76,6 +91,12 @@ func handleProviderRequest(
// such as dew point and wind chill // such as dew point and wind chill
update.Enrich() update.Enrich()
// Update metrics
if aw.metrics == nil {
aw.metrics = weather.MustInitMetrics(aw.appCtx)
}
aw.metrics.Update(update)
l.Debug(). l.Debug().
Str("provider", p.Name()). Str("provider", p.Name()).
Any("update", update). Any("update", update).

View File

@ -41,13 +41,13 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate {
return &weather.WeatherUpdate{ return &weather.WeatherUpdate{
StationType: awnUpdate.StationType, StationType: awnUpdate.StationType,
DateUTC: &updateTime, DateUTC: &updateTime,
TempF: awnUpdate.TempF, TempOutdoorF: awnUpdate.TempF,
Humidity: awnUpdate.Humidity, HumidityOudoor: awnUpdate.Humidity,
WindSpeedMPH: awnUpdate.WindGustMPH, WindSpeedMPH: awnUpdate.WindGustMPH,
WindGustMPH: awnUpdate.WindGustMPH, WindGustMPH: awnUpdate.WindGustMPH,
MaxDailyGust: awnUpdate.MaxDailyGust, MaxDailyGust: awnUpdate.MaxDailyGust,
WindDir: awnUpdate.WindDir, WindDir: awnUpdate.WindDir,
WindDirAVG10m: awnUpdate.WindDirAVG10m, WindDirAvg10m: awnUpdate.WindDirAVG10m,
UV: awnUpdate.UV, UV: awnUpdate.UV,
SolarRadiation: awnUpdate.SolarRadiation, SolarRadiation: awnUpdate.SolarRadiation,
HourlyRainIn: awnUpdate.HourlyRainIn, HourlyRainIn: awnUpdate.HourlyRainIn,
@ -60,8 +60,8 @@ func MapAwnUpdate(awnUpdate *AmbientWeatherUpdate) *weather.WeatherUpdate {
BattOutdoorSensor: awnUpdate.BattOut, BattOutdoorSensor: awnUpdate.BattOut,
BattIndoorSensor: awnUpdate.BattIn, BattIndoorSensor: awnUpdate.BattIn,
BattRainSensor: awnUpdate.BattRain, BattRainSensor: awnUpdate.BattRain,
TempInsideF: awnUpdate.TempInF, TempIndoorF: awnUpdate.TempInF,
HumidityInside: awnUpdate.HumidityIn, HumidityIndoor: awnUpdate.HumidityIn,
BaromRelativeIn: awnUpdate.BaromRelIn, BaromRelativeIn: awnUpdate.BaromRelIn,
BaromAbsoluteIn: awnUpdate.BaromAbsIn, BaromAbsoluteIn: awnUpdate.BaromAbsIn,
} }

View File

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

View File

@ -41,8 +41,8 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate {
return &weather.WeatherUpdate{ return &weather.WeatherUpdate{
StationType: wuUpdate.SoftwareType, StationType: wuUpdate.SoftwareType,
DateUTC: &updateTime, DateUTC: &updateTime,
TempF: wuUpdate.Tempf, TempOutdoorF: wuUpdate.Tempf,
Humidity: wuUpdate.Humidity, HumidityOudoor: wuUpdate.Humidity,
WindSpeedMPH: wuUpdate.WindGustMPH, WindSpeedMPH: wuUpdate.WindGustMPH,
WindGustMPH: wuUpdate.WindGustMPH, WindGustMPH: wuUpdate.WindGustMPH,
WindDir: wuUpdate.WindDir, WindDir: wuUpdate.WindDir,
@ -53,8 +53,8 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate {
WeeklyRainIn: wuUpdate.WeeklyRainIn, WeeklyRainIn: wuUpdate.WeeklyRainIn,
MonthlyRainIn: wuUpdate.MonthlyRainIn, MonthlyRainIn: wuUpdate.MonthlyRainIn,
YearlyRainIn: wuUpdate.YearlyRainIn, YearlyRainIn: wuUpdate.YearlyRainIn,
TempInsideF: wuUpdate.IndoorTempF, TempIndoorF: wuUpdate.IndoorTempF,
HumidityInside: wuUpdate.IndoorHumidity, HumidityIndoor: wuUpdate.IndoorHumidity,
BaromRelativeIn: wuUpdate.BaromIn, BaromRelativeIn: wuUpdate.BaromIn,
} }
} }

View File

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

View File

@ -4,18 +4,13 @@ import "math"
// Attempts to complete missing fields that may not // Attempts to complete missing fields that may not
// be set by a specific provider, such as DewPoint and WindChill // be set by a specific provider, such as DewPoint and WindChill
//
// 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() { func (u *WeatherUpdate) Enrich() {
if u.WindChillF == 0 { if u.WindChillF == 0 {
u.WindChillF = CalculateWindChill(u.TempF, u.WindSpeedMPH) u.WindChillF = CalculateWindChill(u.TempOutdoorF, u.WindSpeedMPH)
} }
if u.DewPointF == 0 { if u.DewPointF == 0 {
u.DewPointF = CalculateDewPoint(u.TempF, float32(u.Humidity)) u.DewPointF = CalculateDewPoint(u.TempOutdoorF, float64(u.HumidityOudoor))
} }
if u.BaromAbsoluteIn == 0 { 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 // Convert temperature from Fahrenheit to Celsius
tempC := (tempF - 32) * 5 / 9 tempC := (tempF - 32) * 5 / 9
// Calculate the dew point using the Magnus-Tetens approximation // Calculate the dew point using the Magnus-Tetens approximation
a := float32(17.27) a := float64(17.27)
b := float32(237.7) 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) dewPointC := (b * alpha) / (a - alpha)
// Convert dew point back to Fahrenheit // Convert dew point back to Fahrenheit
@ -39,16 +34,14 @@ func CalculateDewPoint(tempF, humidity float32) float32 {
return dewPointF return dewPointF
} }
func CalculateWindChill(tempF float32, windSpeedMPH float32) float32 { func CalculateWindChill(tempF float64, windSpeedMPH float64) float64 {
if windSpeedMPH <= 3 { if windSpeedMPH <= 3 {
// Wind chill calculation doesn't apply for very low wind speeds // Wind chill calculation doesn't apply for very low wind speeds
return tempF return tempF
} }
// Formula for calculating wind chill // Formula for calculating wind chill
return float32( return 35.74 + 0.6215*tempF -
35.74 + 0.6215*float64(tempF) - 35.75*math.Pow(windSpeedMPH, 0.16) +
35.75*math.Pow(float64(windSpeedMPH), 0.16) + 0.4275*tempF*math.Pow(windSpeedMPH, 0.16)
0.4275*float64(tempF)*
math.Pow(float64(windSpeedMPH), 0.16))
} }

View File

@ -1,3 +1,148 @@
package weather 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)
}

View File

@ -9,31 +9,31 @@ import (
type WeatherUpdate struct { type WeatherUpdate struct {
DateUTC *time.Time DateUTC *time.Time
StationType string StationType string
TempF float32 TempOutdoorF float64
TempInsideF float32 TempIndoorF float64
Humidity int HumidityOudoor int
HumidityInside int HumidityIndoor int
WindSpeedMPH float32 WindSpeedMPH float64
WindGustMPH float32 WindGustMPH float64
MaxDailyGust float32 MaxDailyGust float64
WindDir int WindDir int
WindDirAVG10m int WindDirAvg10m int
UV int UV int
SolarRadiation float32 SolarRadiation float64
HourlyRainIn float32 HourlyRainIn float64
EventRainIn float32 EventRainIn float64
DailyRainIn float32 DailyRainIn float64
WeeklyRainIn float32 WeeklyRainIn float64
MonthlyRainIn float32 MonthlyRainIn float64
YearlyRainIn float32 YearlyRainIn float64
TotalRainIn float32 TotalRainIn float64
BattOutdoorSensor int BattOutdoorSensor int
BattIndoorSensor int BattIndoorSensor int
BattRainSensor int BattRainSensor int
BaromRelativeIn float32 BaromRelativeIn float64
BaromAbsoluteIn float32 BaromAbsoluteIn float64
// These fields may be calculated // These fields may be calculated
// if not otherwise set // if not otherwise set
DewPointF float32 DewPointF float64
WindChillF float32 WindChillF float64
} }