summarise data from openmeteo
This commit is contained in:
219
cmd/ingestd/forecast_logging.go
Normal file
219
cmd/ingestd/forecast_logging.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-weatherstation/internal/mqttingest"
|
||||
"go-weatherstation/internal/providers"
|
||||
)
|
||||
|
||||
type ForecastCache struct {
|
||||
mu sync.RWMutex
|
||||
res *providers.ForecastResult
|
||||
}
|
||||
|
||||
func (c *ForecastCache) Set(res *providers.ForecastResult) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.res = copyForecast(res)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *ForecastCache) Get() *providers.ForecastResult {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
c.mu.RLock()
|
||||
res := c.res
|
||||
c.mu.RUnlock()
|
||||
return res
|
||||
}
|
||||
|
||||
func copyForecast(res *providers.ForecastResult) *providers.ForecastResult {
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
out := &providers.ForecastResult{
|
||||
RetrievedAt: res.RetrievedAt,
|
||||
Model: res.Model,
|
||||
}
|
||||
if len(res.Hourly) > 0 {
|
||||
out.Hourly = make([]providers.HourlyForecastPoint, len(res.Hourly))
|
||||
copy(out.Hourly, res.Hourly)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func forecastSummary(res *providers.ForecastResult) string {
|
||||
if res == nil || len(res.Hourly) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
start := res.Hourly[0].TS
|
||||
end := res.Hourly[len(res.Hourly)-1].TS
|
||||
parts := []string{
|
||||
fmt.Sprintf("range=%s..%s", start.Format(time.RFC3339), end.Format(time.RFC3339)),
|
||||
}
|
||||
|
||||
var (
|
||||
minTemp, maxTemp float64
|
||||
hasTemp bool
|
||||
maxWind float64
|
||||
hasWind bool
|
||||
maxGust float64
|
||||
hasGust bool
|
||||
totalPrecip float64
|
||||
hasPrecip bool
|
||||
)
|
||||
|
||||
for _, pt := range res.Hourly {
|
||||
if pt.TempC != nil {
|
||||
if !hasTemp {
|
||||
minTemp, maxTemp = *pt.TempC, *pt.TempC
|
||||
hasTemp = true
|
||||
} else {
|
||||
if *pt.TempC < minTemp {
|
||||
minTemp = *pt.TempC
|
||||
}
|
||||
if *pt.TempC > maxTemp {
|
||||
maxTemp = *pt.TempC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pt.WindMS != nil {
|
||||
if !hasWind || *pt.WindMS > maxWind {
|
||||
maxWind = *pt.WindMS
|
||||
hasWind = true
|
||||
}
|
||||
}
|
||||
|
||||
if pt.WindGustMS != nil {
|
||||
if !hasGust || *pt.WindGustMS > maxGust {
|
||||
maxGust = *pt.WindGustMS
|
||||
hasGust = true
|
||||
}
|
||||
}
|
||||
|
||||
if pt.PrecipMM != nil {
|
||||
totalPrecip += *pt.PrecipMM
|
||||
hasPrecip = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasTemp {
|
||||
parts = append(parts, fmt.Sprintf("temp_c(min=%.2f,max=%.2f)", minTemp, maxTemp))
|
||||
}
|
||||
if hasWind {
|
||||
parts = append(parts, fmt.Sprintf("wind_ms(max=%.2f)", maxWind))
|
||||
}
|
||||
if hasGust {
|
||||
parts = append(parts, fmt.Sprintf("gust_ms(max=%.2f)", maxGust))
|
||||
}
|
||||
if hasPrecip {
|
||||
parts = append(parts, fmt.Sprintf("precip_mm(total=%.2f)", totalPrecip))
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func logForecastDeviation(cache *ForecastCache, snap mqttingest.Snapshot) {
|
||||
res := cache.Get()
|
||||
if res == nil || len(res.Hourly) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fc, diff := nearestForecastPoint(res.Hourly, snap.TS)
|
||||
if fc == nil || diff > 2*time.Hour {
|
||||
return
|
||||
}
|
||||
|
||||
parts := []string{
|
||||
fmt.Sprintf("model=%s", res.Model),
|
||||
fmt.Sprintf("obs_ts=%s", snap.TS.Format(time.RFC3339)),
|
||||
fmt.Sprintf("fc_ts=%s", fc.TS.Format(time.RFC3339)),
|
||||
fmt.Sprintf("dt=%s", diff.Round(time.Minute)),
|
||||
}
|
||||
|
||||
if fc.TempC != nil {
|
||||
parts = append(parts, fmt.Sprintf(
|
||||
"temp_c(obs=%.2f fc=%.2f d=%.2f)",
|
||||
snap.P.TemperatureC, *fc.TempC, snap.P.TemperatureC-*fc.TempC,
|
||||
))
|
||||
}
|
||||
|
||||
if fc.WindMS != nil {
|
||||
parts = append(parts, fmt.Sprintf(
|
||||
"wind_ms(obs=%.2f fc=%.2f d=%.2f)",
|
||||
snap.P.WindAvgMS, *fc.WindMS, snap.P.WindAvgMS-*fc.WindMS,
|
||||
))
|
||||
}
|
||||
|
||||
if fc.WindGustMS != nil {
|
||||
parts = append(parts, fmt.Sprintf(
|
||||
"gust_ms(obs=%.2f fc=%.2f d=%.2f)",
|
||||
snap.P.WindMaxMS, *fc.WindGustMS, snap.P.WindMaxMS-*fc.WindGustMS,
|
||||
))
|
||||
}
|
||||
|
||||
if fc.WindDirDeg != nil {
|
||||
dirDelta := angularDiffDeg(snap.P.WindDirDeg, *fc.WindDirDeg)
|
||||
parts = append(parts, fmt.Sprintf(
|
||||
"wind_dir_deg(obs=%.0f fc=%.0f d=%.0f)",
|
||||
snap.P.WindDirDeg, *fc.WindDirDeg, dirDelta,
|
||||
))
|
||||
}
|
||||
|
||||
if fc.PrecipMM != nil {
|
||||
parts = append(parts, fmt.Sprintf(
|
||||
"rain_mm(obs=%.2f fc=%.2f d=%.2f)",
|
||||
snap.RainLastHourMM, *fc.PrecipMM, snap.RainLastHourMM-*fc.PrecipMM,
|
||||
))
|
||||
}
|
||||
|
||||
if len(parts) <= 4 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("forecast deviation %s", strings.Join(parts, " "))
|
||||
}
|
||||
|
||||
func nearestForecastPoint(hourly []providers.HourlyForecastPoint, ts time.Time) (*providers.HourlyForecastPoint, time.Duration) {
|
||||
if len(hourly) == 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
bestIdx := 0
|
||||
bestDiff := absDuration(ts.Sub(hourly[0].TS))
|
||||
for i := 1; i < len(hourly); i++ {
|
||||
diff := absDuration(ts.Sub(hourly[i].TS))
|
||||
if diff < bestDiff {
|
||||
bestIdx = i
|
||||
bestDiff = diff
|
||||
}
|
||||
}
|
||||
|
||||
return &hourly[bestIdx], bestDiff
|
||||
}
|
||||
|
||||
func absDuration(d time.Duration) time.Duration {
|
||||
if d < 0 {
|
||||
return -d
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func angularDiffDeg(a, b float64) float64 {
|
||||
diff := math.Mod(math.Abs(a-b), 360.0)
|
||||
if diff > 180.0 {
|
||||
diff = 360.0 - diff
|
||||
}
|
||||
return diff
|
||||
}
|
||||
Reference in New Issue
Block a user