270 lines
9.5 KiB
Go
270 lines
9.5 KiB
Go
package briefing
|
|
|
|
import (
|
|
"fmt"
|
|
"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, options any) (*module.Output, error) {
|
|
discussion := ctx.Collected.Discussion
|
|
if discussion == nil {
|
|
return nil, nil
|
|
}
|
|
opts, ok := options.(module.AreaForecastDiscussionOptions)
|
|
if !ok {
|
|
return nil, fmt.Errorf("area forecast discussion options have type %T", options)
|
|
}
|
|
sections, err := areaForecastDiscussionSections(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
value := AreaForecastDiscussionModule{}
|
|
if sections["product"] {
|
|
value.Product = discussion.Product
|
|
}
|
|
if sections["key_messages"] {
|
|
value.KeyMessages = append([]string(nil), discussion.KeyMessages...)
|
|
}
|
|
if sections["short_term"] && discussion.ShortTerm != nil {
|
|
value.ShortTerm = discussion.ShortTerm.Text
|
|
}
|
|
if sections["long_term"] && 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 areaForecastDiscussionSections(options module.AreaForecastDiscussionOptions) (map[string]bool, error) {
|
|
if len(options.Sections) == 0 {
|
|
return map[string]bool{
|
|
"product": true,
|
|
"key_messages": true,
|
|
"short_term": true,
|
|
"long_term": true,
|
|
}, nil
|
|
}
|
|
sections := map[string]bool{}
|
|
for _, section := range options.Sections {
|
|
switch section {
|
|
case "product", "key_messages", "short_term", "long_term":
|
|
sections[section] = true
|
|
default:
|
|
return nil, fmt.Errorf("area forecast discussion section %q is not supported", section)
|
|
}
|
|
}
|
|
return sections, 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
|
|
}
|