Refactored the derived_daily_summary module to utilize narrative forecast data where available

This commit is contained in:
2026-06-10 09:35:39 -05:00
parent d1d0df11a8
commit ef044327c6
5 changed files with 193 additions and 54 deletions

View File

@@ -5,22 +5,20 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
"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"`
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
MaxPopWindow string `json:"max_pop_window,omitempty"`
FirstPrecipHour string `json:"first_precip_hour,omitempty"`
LastPrecipHour string `json:"last_precip_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"`
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) {
@@ -46,17 +44,10 @@ func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.Pre
var apparent forecast.Range
var maxPop *forecast.TimedValue
var maxGust *forecast.TimedValue
var maxPopWindow timeutil.Period
for _, daypart := range summary.Dayparts {
addRange(&temperature, daypart.Temperature)
addRange(&apparent, daypart.ApparentTemperature)
if daypart.MaxPrecipitationProbability != nil {
if maxPop == nil || daypart.MaxPrecipitationProbability.Value > maxPop.Value {
copied := *daypart.MaxPrecipitationProbability
maxPop = &copied
maxPopWindow = daypart.Period
}
}
maxTimedValue(&maxPop, daypart.MaxPrecipitationProbability)
maxTimedValue(&maxGust, daypart.PeakWindGust)
if daypart.DominantCondition != "" {
conditions[daypart.DominantCondition] = struct{}{}
@@ -70,19 +61,112 @@ func derivedDailySummaryValue(summary forecast.DailySummary, timing forecast.Pre
hazards[alert.Event] = struct{}{}
}
}
value.HighTempF = roundedInt(temperature.Max)
value.LowTempF = roundedInt(temperature.Min)
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.MaxPopPercent = roundedInt(&maxPop.Value)
value.MaxPopWindow = periodClockLabel(maxPopWindow, timezone)
value.MostLikelyPrecipitationHour = mostLikelyPrecipitationHour(maxPop, timezone)
}
if maxGust != nil {
value.MaxWindGustMph = roundedInt(&maxGust.Value)
}
value.FirstPrecipHour = timedClockLabel(timing.FirstPrecipitation, timezone)
value.LastPrecipHour = timedClockLabel(timing.LastPrecipitation, timezone)
value.DominantConditions = sortedSet(conditions)
value.DominantConditions = narrativeConditions(summary.NarrativePeriods)
if len(value.DominantConditions) == 0 {
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 narrativeConditions(periods []weatherdata.ForecastPeriod) []string {
seen := map[string]struct{}{}
var out []string
for _, period := range periods {
if period.TextDescription == "" {
continue
}
if _, ok := seen[period.TextDescription]; ok {
continue
}
seen[period.TextDescription] = struct{}{}
out = append(out, period.TextDescription)
}
return out
}
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))
}