package briefing import ( "strings" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) const hourlyForecastPrecipMentionProbabilityThreshold = 20 type HourlyForecastModule struct { Product string `json:"product,omitempty"` IssuedAt time.Time `json:"issued_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"` SourceLocation string `json:"source_location,omitempty"` SourceLocationID string `json:"source_location_id,omitempty"` Periods []HourlyForecastPeriod `json:"periods,omitempty"` } type HourlyForecastPromptExport struct { Product string `json:"product,omitempty"` IssuedAt time.Time `json:"issued_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"` SourceLocation string `json:"source_location,omitempty"` SourceLocationID string `json:"source_location_id,omitempty"` Periods []HourlyForecastPromptPeriod `json:"periods,omitempty"` } type HourlyForecastPeriod struct { HourLabel string `json:"hour_label,omitempty"` PeriodBegins string `json:"period_begins,omitempty"` PeriodEnds string `json:"period_ends,omitempty"` Name string `json:"name,omitempty"` IsDay *bool `json:"is_day,omitempty"` ConditionCode *int `json:"condition_code,omitempty"` TextDescription string `json:"text_description,omitempty"` TextDescriptionLower string `json:"text_description_lower,omitempty"` TemperatureC *float64 `json:"temperature_c,omitempty"` TemperatureF *float64 `json:"temperature_f,omitempty"` TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` DewpointC *float64 `json:"dewpoint_c,omitempty"` DewpointF *float64 `json:"dewpoint_f,omitempty"` WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` WindGustMph *float64 `json:"wind_gust_mph,omitempty"` WindDirection string `json:"wind_direction,omitempty"` BarometricPressurePa *float64 `json:"barometric_pressure_pa,omitempty"` BarometricPressureInHg *float64 `json:"barometric_pressure_in_hg,omitempty"` VisibilityMeters *float64 `json:"visibility_meters,omitempty"` VisibilityMiles *float64 `json:"visibility_miles,omitempty"` ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"` ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"` CloudCoverPercent *float64 `json:"cloud_cover_percent,omitempty"` ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` MentionPrecipitation bool `json:"mention_precipitation,omitempty"` PrecipitationAmountMm *float64 `json:"precipitation_amount_mm,omitempty"` PrecipitationAmountIn *float64 `json:"precipitation_amount_in,omitempty"` SnowfallDepthMM *float64 `json:"snowfall_depth_mm,omitempty"` SnowfallDepthIn *float64 `json:"snowfall_depth_in,omitempty"` UVIndex *float64 `json:"uv_index,omitempty"` RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"` } type HourlyForecastPromptPeriod struct { PeriodBegins string `json:"period_begins,omitempty"` PeriodEnds string `json:"period_ends,omitempty"` Name string `json:"name,omitempty"` IsDay *bool `json:"is_day,omitempty"` ConditionCode *int `json:"condition_code,omitempty"` TextDescription string `json:"text_description,omitempty"` TemperatureC *float64 `json:"temperature_c,omitempty"` TemperatureF *float64 `json:"temperature_f,omitempty"` TemperatureCMin *float64 `json:"temperature_c_min,omitempty"` TemperatureFMin *float64 `json:"temperature_f_min,omitempty"` TemperatureCMax *float64 `json:"temperature_c_max,omitempty"` TemperatureFMax *float64 `json:"temperature_f_max,omitempty"` DewpointC *float64 `json:"dewpoint_c,omitempty"` DewpointF *float64 `json:"dewpoint_f,omitempty"` WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"` WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"` WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"` WindGustMph *float64 `json:"wind_gust_mph,omitempty"` WindDirection string `json:"wind_direction,omitempty"` BarometricPressurePa *float64 `json:"barometric_pressure_pa,omitempty"` BarometricPressureInHg *float64 `json:"barometric_pressure_in_hg,omitempty"` VisibilityMeters *float64 `json:"visibility_meters,omitempty"` VisibilityMiles *float64 `json:"visibility_miles,omitempty"` ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"` ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"` CloudCoverPercent *float64 `json:"cloud_cover_percent,omitempty"` ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"` PrecipitationAmountMm *float64 `json:"precipitation_amount_mm,omitempty"` PrecipitationAmountIn *float64 `json:"precipitation_amount_in,omitempty"` SnowfallDepthMM *float64 `json:"snowfall_depth_mm,omitempty"` SnowfallDepthIn *float64 `json:"snowfall_depth_in,omitempty"` UVIndex *float64 `json:"uv_index,omitempty"` RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"` } func buildHourlyForecastModule(ctx ModuleContext, _ any) (*module.Output, error) { hourly := ctx.Collected.Hourly if hourly == nil || len(ctx.Derived.ValidPeriodHourlyPeriods) == 0 { return nil, nil } value := HourlyForecastModule{ Product: hourly.Product, IssuedAt: hourly.IssuedAt, UpdatedAt: copyTime(hourly.UpdatedAt), SourceLocation: hourly.LocationName, SourceLocationID: hourly.LocationID, Periods: hourlyForecastPeriods(ctx.Derived.ValidPeriodHourlyPeriods, ctx.Timezone), } if value.isEmpty() { return nil, nil } return &module.Output{ID: module.HourlyForecast, StanzaName: "hourly_forecast", Value: value}, nil } func exportHourlyForecastPromptValue(value any) (any, error) { rich, ok := value.(HourlyForecastModule) if !ok { return nil, unexpectedPromptExportValue(value, HourlyForecastModule{}) } return HourlyForecastPromptExport{ Product: rich.Product, IssuedAt: rich.IssuedAt, UpdatedAt: copyTime(rich.UpdatedAt), SourceLocation: rich.SourceLocation, SourceLocationID: rich.SourceLocationID, Periods: hourlyForecastPromptPeriods(rich.Periods), }, nil } func hourlyForecastPromptPeriods(periods []HourlyForecastPeriod) []HourlyForecastPromptPeriod { if len(periods) == 0 { return nil } out := make([]HourlyForecastPromptPeriod, 0, len(periods)) for _, period := range periods { out = append(out, HourlyForecastPromptPeriod{ PeriodBegins: period.PeriodBegins, PeriodEnds: period.PeriodEnds, Name: period.Name, IsDay: copyBool(period.IsDay), ConditionCode: copyInt(period.ConditionCode), TextDescription: period.TextDescription, TemperatureC: copyFloat(period.TemperatureC), TemperatureF: copyFloat(period.TemperatureF), TemperatureCMin: copyFloat(period.TemperatureCMin), TemperatureFMin: copyFloat(period.TemperatureFMin), TemperatureCMax: copyFloat(period.TemperatureCMax), TemperatureFMax: copyFloat(period.TemperatureFMax), DewpointC: copyFloat(period.DewpointC), DewpointF: copyFloat(period.DewpointF), WindSpeedKmh: copyFloat(period.WindSpeedKmh), WindSpeedMph: copyFloat(period.WindSpeedMph), WindGustKmh: copyFloat(period.WindGustKmh), WindGustMph: copyFloat(period.WindGustMph), WindDirection: period.WindDirection, BarometricPressurePa: copyFloat(period.BarometricPressurePa), BarometricPressureInHg: copyFloat(period.BarometricPressureInHg), VisibilityMeters: copyFloat(period.VisibilityMeters), VisibilityMiles: copyFloat(period.VisibilityMiles), ApparentTemperatureC: copyFloat(period.ApparentTemperatureC), ApparentTemperatureF: copyFloat(period.ApparentTemperatureF), CloudCoverPercent: copyFloat(period.CloudCoverPercent), ProbabilityOfPrecipitationPercent: copyFloat(period.ProbabilityOfPrecipitationPercent), PrecipitationAmountMm: copyFloat(period.PrecipitationAmountMm), PrecipitationAmountIn: copyFloat(period.PrecipitationAmountIn), SnowfallDepthMM: copyFloat(period.SnowfallDepthMM), SnowfallDepthIn: copyFloat(period.SnowfallDepthIn), UVIndex: copyFloat(period.UVIndex), RelativeHumidityPercent: copyFloat(period.RelativeHumidityPercent), }) } return out } func hourlyForecastPeriods(periods []weatherdata.ForecastPeriod, timezone string) []HourlyForecastPeriod { return hourlyForecastPeriodsWithPrecipMentionThreshold(periods, timezone, hourlyForecastPrecipMentionProbabilityThreshold) } func hourlyForecastPeriodsWithPrecipMentionThreshold(periods []weatherdata.ForecastPeriod, timezone string, threshold float64) []HourlyForecastPeriod { out := make([]HourlyForecastPeriod, 0, len(periods)) for _, period := range periods { validPeriod := timeutil.Period{Start: period.StartTime, End: period.EndTime} out = append(out, HourlyForecastPeriod{ HourLabel: hourMinuteLabel(period.StartTime, timezone), PeriodBegins: friendlyPeriodBeginsLabel(validPeriod, timezone), PeriodEnds: friendlyPeriodEndsLabel(validPeriod, timezone), Name: period.Name, IsDay: copyBool(period.IsDay), ConditionCode: copyInt(period.ConditionCode), TextDescription: period.TextDescription, TextDescriptionLower: strings.ToLower(period.TextDescription), TemperatureC: copyFloat(period.TemperatureC), TemperatureF: copyFloat(period.TemperatureF), TemperatureCMin: copyFloat(period.TemperatureCMin), TemperatureFMin: copyFloat(period.TemperatureFMin), TemperatureCMax: copyFloat(period.TemperatureCMax), TemperatureFMax: copyFloat(period.TemperatureFMax), DewpointC: copyFloat(period.DewpointC), DewpointF: copyFloat(period.DewpointF), WindSpeedKmh: copyFloat(period.WindSpeedKmh), WindSpeedMph: copyFloat(period.WindSpeedMph), WindGustKmh: copyFloat(period.WindGustKmh), WindGustMph: copyFloat(period.WindGustMph), WindDirection: windDirectionLabel(period.WindDirectionDegrees), BarometricPressurePa: copyFloat(period.BarometricPressurePa), BarometricPressureInHg: copyFloat(period.BarometricPressureInHg), VisibilityMeters: copyFloat(period.VisibilityMeters), VisibilityMiles: copyFloat(period.VisibilityMiles), ApparentTemperatureC: copyFloat(period.ApparentTemperatureC), ApparentTemperatureF: copyFloat(period.ApparentTemperatureF), CloudCoverPercent: copyFloat(period.CloudCoverPercent), ProbabilityOfPrecipitationPercent: copyFloat(period.ProbabilityOfPrecipitationPercent), MentionPrecipitation: mentionHourlyForecastPrecipitation(period.ProbabilityOfPrecipitationPercent, threshold), PrecipitationAmountMm: copyFloat(period.PrecipitationAmountMm), PrecipitationAmountIn: copyFloat(period.PrecipitationAmountIn), SnowfallDepthMM: copyFloat(period.SnowfallDepthMM), SnowfallDepthIn: copyFloat(period.SnowfallDepthIn), UVIndex: copyFloat(period.UVIndex), RelativeHumidityPercent: copyFloat(period.RelativeHumidityPercent), }) } return out } func mentionHourlyForecastPrecipitation(probability *float64, threshold float64) bool { return probability != nil && *probability >= threshold } func (v HourlyForecastModule) isEmpty() bool { return v.Product == "" && v.IssuedAt.IsZero() && v.UpdatedAt == nil && v.SourceLocation == "" && v.SourceLocationID == "" && len(v.Periods) == 0 }