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" ) type AWNProvider struct{} const ( providerName = "awn" awnURL = "http://ambientweather.net/data/report" ) func (awn *AWNProvider) Name() string { return providerName } // Takes an inbound request from the ambient device and maps // to a stable struct for weather updates func (awn *AWNProvider) ReqToWeather(_ context.Context, r *http.Request) ( *weather.WeatherUpdate, error, ) { awnUpdate, err := UnmarshalQueryParams(r.URL.Query()) if err != nil { return nil, err } 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 { ut, err := time.Parse(time.DateTime, *awnUpdate.DateUTC) if err == nil { updateTime = ut } } credentials := make(map[string]string) if awnUpdate.PassKey != nil { credentials["PASSKEY"] = *awnUpdate.PassKey } return &weather.WeatherUpdate{ DateUTC: &updateTime, StationID: awnUpdate.PassKey, StationType: awnUpdate.StationType, TempOutdoorF: awnUpdate.TempF, HumidityOudoor: awnUpdate.Humidity, WindSpeedMPH: awnUpdate.WindSpeedMPH, WindGustMPH: awnUpdate.WindGustMPH, MaxDailyGust: awnUpdate.MaxDailyGust, WindDir: awnUpdate.WindDir, WindDirAvg10m: awnUpdate.WindDirAVG10m, UV: awnUpdate.UV, SolarRadiation: awnUpdate.SolarRadiation, HourlyRainIn: awnUpdate.HourlyRainIn, EventRainIn: awnUpdate.EventRainIn, DailyRainIn: awnUpdate.DailyRainIn, WeeklyRainIn: awnUpdate.WeeklyRainIn, MonthlyRainIn: awnUpdate.MonthlyRainIn, YearlyRainIn: awnUpdate.YearlyRainIn, TotalRainIn: awnUpdate.TotalRainIn, Batteries: []weather.BatteryStatus{ { Component: "OutdoorSensor", Status: awnUpdate.BattOut, }, { Component: "IndoorSensor", Status: awnUpdate.BattIn, }, { Component: "RainSensor", Status: awnUpdate.BattRain, }, { Component: "CO2Sensor", Status: awnUpdate.BattCO2, }, }, TempIndoorF: awnUpdate.TempInF, HumidityIndoor: awnUpdate.HumidityIn, BaromRelativeIn: awnUpdate.BaromRelIn, BaromAbsoluteIn: awnUpdate.BaromAbsIn, WeatherServiceCredentials: credentials, } } func UnmarshalQueryParams(query url.Values) (*AmbientWeatherUpdate, error) { update := new(AmbientWeatherUpdate) decoder := schema.NewDecoder() if err := decoder.Decode(update, query); err != nil { return nil, err } return update, nil }