package briefing import ( "fmt" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) type DerivedDailySummaryModule struct { Date string `json:"date,omitempty"` HighTempF *int `json:"high_temp_f,omitempty"` LowTempF *int `json:"low_temp_f,omitempty"` DailyPrecipitationProbability *int `json:"daily_precipitation_probability,omitempty"` MostLikelyPrecipitationHour string `json:"most_likely_precipitation_hour,omitempty"` ThunderMentioned bool `json:"thunder_mentioned"` MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"` HeatIndexMaxF *int `json:"heat_index_max_f,omitempty"` DominantConditions []string `json:"dominant_conditions,omitempty"` Hazards []string `json:"hazards,omitempty"` } func buildDerivedDailySummaryModule(ctx ModuleContext, _ any) (*module.Output, error) { summary := ctx.Derived.FirstDailySummary() if summary == nil { return nil, fmt.Errorf("daily summary facts are required") } value, err := derivedDailySummaryValue(*summary, ctx.Derived.PrecipTiming, ctx.Timezone) if err != nil { return nil, err } return &module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: value}, nil } func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.PrecipTiming, timezone string) (DerivedDailySummaryModule, error) { value := DerivedDailySummaryModule{ Date: friendlyDateLabel(summary.Date, timezone), ThunderMentioned: timing.ThunderMentioned, } conditions := map[string]struct{}{} hazards := map[string]struct{}{} var temperature forecast.Range var apparent forecast.Range var maxPop *forecast.TimedValue var maxGust *forecast.TimedValue for _, daypart := range summary.Dayparts { addRange(&temperature, daypart.Temperature) addRange(&apparent, daypart.ApparentTemperature) maxTimedValue(&maxPop, daypart.MaxPrecipitationProbability) maxTimedValue(&maxGust, daypart.PeakWindGust) if daypart.DominantCondition != "" { conditions[daypart.DominantCondition] = struct{}{} } for _, hazard := range hazardsForIndicators(daypart.Indicators) { hazards[hazard] = struct{}{} } } for _, alert := range summary.AlertOverlaps { if alert.Event != "" { hazards[alert.Event] = struct{}{} } } narrativeTemperature := narrativeTemperatureRange(summary.NarrativePeriods) if narrativeTemperature.Max != nil { value.HighTempF = roundedInt(narrativeTemperature.Max) } else { value.HighTempF = roundedInt(temperature.Max) } if narrativeTemperature.Min != nil { value.LowTempF = roundedInt(narrativeTemperature.Min) } else { value.LowTempF = roundedInt(temperature.Min) } value.HeatIndexMaxF = roundedInt(apparent.Max) narrativePrecipitation := narrativeMaxPrecipitation(summary.NarrativePeriods) if narrativePrecipitation != nil { value.DailyPrecipitationProbability = roundedInt(&narrativePrecipitation.Value) } else if maxPop != nil { value.DailyPrecipitationProbability = roundedInt(&maxPop.Value) } if maxPop != nil { value.MostLikelyPrecipitationHour = mostLikelyPrecipitationHour(maxPop, timezone) } if maxGust != nil { value.MaxWindGustMph = roundedInt(&maxGust.Value) } value.DominantConditions = sortedSet(conditions) value.Hazards = sortedSet(hazards) return value, nil } func narrativeTemperatureRange(periods []weatherdata.ForecastPeriod) forecast.Range { var out forecast.Range for _, period := range periods { addNarrativeHigh(&out, period.TemperatureFMax) addNarrativeLow(&out, period.TemperatureFMin) if period.TemperatureF != nil && period.IsDay != nil { if *period.IsDay { addNarrativeHigh(&out, period.TemperatureF) } else { addNarrativeLow(&out, period.TemperatureF) } } } return out } func addNarrativeHigh(target *forecast.Range, value *float64) { if value == nil { return } if target.Max == nil || *value > *target.Max { copied := *value target.Max = &copied } } func addNarrativeLow(target *forecast.Range, value *float64) { if value == nil { return } if target.Min == nil || *value < *target.Min { copied := *value target.Min = &copied } } func narrativeMaxPrecipitation(periods []weatherdata.ForecastPeriod) *forecast.TimedValue { var maxPop *forecast.TimedValue for _, period := range periods { if period.ProbabilityOfPrecipitationPercent == nil { continue } value := forecast.TimedValue{ Value: *period.ProbabilityOfPrecipitationPercent, Time: period.StartTime, } maxTimedValue(&maxPop, &value) } return maxPop } func mostLikelyPrecipitationHour(maxPop *forecast.TimedValue, timezone string) string { if maxPop == nil || maxPop.Value <= 0 { return "" } percent := roundedInt(&maxPop.Value) if percent == nil { return "" } return fmt.Sprintf("%d%% at %s", *percent, clockLabel(maxPop.Time, timezone)) }