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 }