Files
weatherreporter/internal/briefing/base_modules.go

237 lines
8.5 KiB
Go

package briefing
import (
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
type MetadataModule struct {
RunID string `json:"run_id"`
ReportID report.ID `json:"report_id"`
Variant string `json:"variant,omitempty"`
PromptID string `json:"prompt_id"`
GeneratedAt time.Time `json:"generated_at"`
Units string `json:"units"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"valid_period"`
Location *LocationContext `json:"location,omitempty"`
SourceWarnings []SourceWarningSummary `json:"source_warnings,omitempty"`
Alerts *AlertDigestModule `json:"alerts,omitempty"`
}
type SourceWarningSummary struct {
Source string `json:"source"`
Code string `json:"code"`
Severity string `json:"severity"`
Message string `json:"message"`
CompletenessImpact string `json:"completeness_impact,omitempty"`
}
type CurrentConditionsModule struct {
ConditionText string `json:"condition_text,omitempty"`
IsDay *bool `json:"is_day,omitempty"`
TemperatureC *float64 `json:"temperature_c,omitempty"`
TemperatureF *float64 `json:"temperature_f,omitempty"`
ApparentTemperatureC *float64 `json:"apparent_temperature_c,omitempty"`
ApparentTemperatureF *float64 `json:"apparent_temperature_f,omitempty"`
DewpointC *float64 `json:"dewpoint_c,omitempty"`
DewpointF *float64 `json:"dewpoint_f,omitempty"`
RelativeHumidityPercent *float64 `json:"relative_humidity_percent,omitempty"`
WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"`
WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"`
WindDirectionDegrees *float64 `json:"wind_direction_degrees,omitempty"`
}
type AlertDigestModule struct {
Checked bool `json:"checked"`
ActiveCount int `json:"active_count"`
RelevantCount int `json:"relevant_count"`
Missing bool `json:"missing,omitempty"`
Relevant []AlertSummary `json:"relevant,omitempty"`
}
type AlertSummary struct {
Event string `json:"event,omitempty"`
Headline string `json:"headline,omitempty"`
Severity string `json:"severity,omitempty"`
}
type AreaForecastDiscussionModule struct {
Product string `json:"product,omitempty"`
KeyMessages []string `json:"key_messages,omitempty"`
ShortTerm string `json:"short_term,omitempty"`
LongTerm string `json:"long_term,omitempty"`
}
type WeatherStoryModule struct {
Available bool `json:"available"`
OfficeID string `json:"office_id,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
AltText string `json:"alt_text,omitempty"`
Priority bool `json:"priority"`
Order int `json:"order"`
DownloadURL string `json:"download_url,omitempty"`
}
func buildMetadataModule(ctx ModuleContext, _ any) (*module.Output, error) {
metadata := ctx.Resolved.Metadata()
value := MetadataModule{
RunID: metadata.RunID,
ReportID: metadata.ReportID,
Variant: variantForReport(metadata.ReportID),
PromptID: metadata.PromptID,
GeneratedAt: metadata.GeneratedAt,
Units: ctx.Units,
Timezone: ctx.Timezone,
ValidPeriod: metadata.ValidPeriod,
Location: copyLocation(ctx.Location),
SourceWarnings: sourceWarningSummaries(ctx.Collected.SourceWarnings),
Alerts: alertDigest(ctx.Collected, ctx.Derived.AlertOverlaps),
}
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: value}, nil
}
func buildCurrentConditionsModule(ctx ModuleContext, _ any) (*module.Output, error) {
current := ctx.Collected.Current
if current == nil {
return nil, nil
}
value := CurrentConditionsModule{
ConditionText: current.ConditionText,
IsDay: copyBool(current.IsDay),
TemperatureC: copyFloat(current.TemperatureC),
TemperatureF: copyFloat(current.TemperatureF),
ApparentTemperatureC: copyFloat(current.ApparentTemperatureC),
ApparentTemperatureF: copyFloat(current.ApparentTemperatureF),
DewpointC: copyFloat(current.DewpointC),
DewpointF: copyFloat(current.DewpointF),
RelativeHumidityPercent: copyFloat(current.RelativeHumidityPercent),
WindSpeedKmh: copyFloat(current.WindSpeedKmh),
WindSpeedMph: copyFloat(current.WindSpeedMph),
WindDirectionDegrees: copyFloat(current.WindDirectionDegrees),
}
if value.isEmpty() {
return nil, nil
}
return &module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: value}, nil
}
func buildAlertDigestModule(ctx ModuleContext, _ any) (*module.Output, error) {
value := alertDigest(ctx.Collected, ctx.Derived.AlertOverlaps)
if value == nil {
value = &AlertDigestModule{}
}
return &module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: *value}, nil
}
func buildAreaForecastDiscussionModule(ctx ModuleContext, _ any) (*module.Output, error) {
discussion := ctx.Collected.Discussion
if discussion == nil {
return nil, nil
}
value := AreaForecastDiscussionModule{
Product: discussion.Product,
KeyMessages: append([]string(nil), discussion.KeyMessages...),
}
if discussion.ShortTerm != nil {
value.ShortTerm = discussion.ShortTerm.Text
}
if discussion.LongTerm != nil {
value.LongTerm = discussion.LongTerm.Text
}
if value.Product == "" && len(value.KeyMessages) == 0 && value.ShortTerm == "" && value.LongTerm == "" {
return nil, nil
}
return &module.Output{ID: module.AreaForecastDiscussion, StanzaName: "area_forecast_discussion", Value: value}, nil
}
func buildWeatherStoryModule(ctx ModuleContext, _ any) (*module.Output, error) {
story := ctx.Collected.WeatherStory
if story == nil {
return nil, nil
}
value := WeatherStoryModule{
Available: true,
OfficeID: story.OfficeID,
StartTime: story.StartTime,
EndTime: story.EndTime,
UpdatedAt: copyTime(story.UpdatedAt),
Title: story.Title,
Description: story.Description,
AltText: story.AltText,
Priority: story.Priority,
Order: story.Order,
DownloadURL: story.DownloadURL,
}
return &module.Output{ID: module.WeatherStory, StanzaName: "weather_story", Value: value}, nil
}
func sourceWarningSummaries(warnings []weatherdata.SourceWarning) []SourceWarningSummary {
out := make([]SourceWarningSummary, 0, len(warnings))
for _, warning := range warnings {
out = append(out, SourceWarningSummary{
Source: warning.Source,
Code: warning.Code,
Severity: warning.Severity,
Message: warning.Message,
CompletenessImpact: warning.CompletenessImpact,
})
}
return out
}
func alertDigest(collected facts.CollectedFacts, overlaps []forecast.AlertOverlap) *AlertDigestModule {
missing := sourceMissing(collected.SourceProvenance, "alerts")
if collected.Alerts == nil && !missing {
return nil
}
value := &AlertDigestModule{Missing: missing}
if collected.Alerts != nil {
value.Checked = true
value.ActiveCount = len(collected.Alerts.Alerts)
}
value.RelevantCount = len(overlaps)
for _, overlap := range overlaps {
value.Relevant = append(value.Relevant, AlertSummary{
Event: overlap.Event,
Headline: overlap.Headline,
Severity: overlap.Severity,
})
}
return value
}
func sourceMissing(sources []weatherdata.Source, name string) bool {
for _, source := range sources {
if source.Name == name && source.Missing {
return true
}
}
return false
}
func (v CurrentConditionsModule) isEmpty() bool {
return v.ConditionText == "" &&
v.IsDay == nil &&
v.TemperatureC == nil &&
v.TemperatureF == nil &&
v.ApparentTemperatureC == nil &&
v.ApparentTemperatureF == nil &&
v.DewpointC == nil &&
v.DewpointF == nil &&
v.RelativeHumidityPercent == nil &&
v.WindSpeedKmh == nil &&
v.WindSpeedMph == nil &&
v.WindDirectionDegrees == nil
}