summarise data from openmeteo

This commit is contained in:
2026-01-26 13:20:30 +11:00
parent c315811ec3
commit 7526a7af93
3 changed files with 246 additions and 6 deletions

View 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
}