270 lines
8.3 KiB
Go
270 lines
8.3 KiB
Go
// Package briefing builds report-specific structured briefing packages.
|
|
package briefing
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
|
)
|
|
|
|
const SchemaVersion = "weatherreporter.briefing.v1"
|
|
|
|
type Package struct {
|
|
Metadata Metadata `json:"metadata"`
|
|
CurrentConditions *CurrentConditionsContext `json:"currentConditions,omitempty"`
|
|
Daily *Daily `json:"daily,omitempty"`
|
|
ThreeDay *ThreeDay `json:"threeDay,omitempty"`
|
|
Weekend *Weekend `json:"weekend,omitempty"`
|
|
Storm *Storm `json:"storm,omitempty"`
|
|
}
|
|
|
|
type Metadata struct {
|
|
SchemaVersion string `json:"schemaVersion"`
|
|
RunID string `json:"runId"`
|
|
ReportID report.ID `json:"reportId"`
|
|
Variant string `json:"variant,omitempty"`
|
|
PromptID string `json:"promptId"`
|
|
GeneratedAt time.Time `json:"generatedAt"`
|
|
Units string `json:"units"`
|
|
Timezone string `json:"timezone"`
|
|
ValidPeriod timeutil.Period `json:"validPeriod"`
|
|
Location *LocationContext `json:"location,omitempty"`
|
|
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
|
SourceLocation string `json:"sourceLocation,omitempty"`
|
|
Sources []SourceMetadata `json:"sources,omitempty"`
|
|
SourceWarnings []forecast.SourceWarning `json:"sourceWarnings,omitempty"`
|
|
Alerts *AlertStatus `json:"alerts,omitempty"`
|
|
}
|
|
|
|
type LocationContext struct {
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
Timezone string `json:"timezone,omitempty"`
|
|
}
|
|
|
|
type CurrentConditionsContext struct {
|
|
ConditionText string `json:"conditionText,omitempty"`
|
|
IsDay *bool `json:"isDay,omitempty"`
|
|
TemperatureC *float64 `json:"temperatureC,omitempty"`
|
|
TemperatureF *float64 `json:"temperatureF,omitempty"`
|
|
ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"`
|
|
ApparentTemperatureF *float64 `json:"apparentTemperatureF,omitempty"`
|
|
DewpointC *float64 `json:"dewpointC,omitempty"`
|
|
DewpointF *float64 `json:"dewpointF,omitempty"`
|
|
RelativeHumidityPercent *float64 `json:"relativeHumidityPercent,omitempty"`
|
|
WindSpeedKmh *float64 `json:"windSpeedKmh,omitempty"`
|
|
WindSpeedMph *float64 `json:"windSpeedMph,omitempty"`
|
|
WindDirectionDegrees *float64 `json:"windDirectionDegrees,omitempty"`
|
|
}
|
|
|
|
type SourceMetadata struct {
|
|
Name string `json:"name"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
FetchedAt time.Time `json:"fetchedAt"`
|
|
IssuedAt *time.Time `json:"issuedAt,omitempty"`
|
|
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
|
DataSHA256 string `json:"dataSha256,omitempty"`
|
|
Missing bool `json:"missing,omitempty"`
|
|
Warnings []forecast.SourceWarning `json:"warnings,omitempty"`
|
|
}
|
|
|
|
type AlertStatus struct {
|
|
Checked bool `json:"checked"`
|
|
ActiveCount int `json:"activeCount"`
|
|
RelevantCount int `json:"relevantCount"`
|
|
Missing bool `json:"missing,omitempty"`
|
|
}
|
|
|
|
type BuildContext struct {
|
|
Resolved report.Resolved
|
|
Bundle *forecast.Bundle
|
|
Units string
|
|
Timezone string
|
|
Location *LocationContext
|
|
}
|
|
|
|
func BuildMetadata(ctx BuildContext) Metadata {
|
|
metadata := ctx.Resolved.Metadata()
|
|
sourceLocationID, sourceLocation := sourceLocation(ctx.Bundle)
|
|
return Metadata{
|
|
SchemaVersion: SchemaVersion,
|
|
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),
|
|
SourceLocationID: sourceLocationID,
|
|
SourceLocation: sourceLocation,
|
|
Sources: sourceMetadata(ctx.Bundle),
|
|
SourceWarnings: sourceWarnings(ctx.Bundle),
|
|
Alerts: alertStatus(ctx.Bundle),
|
|
}
|
|
}
|
|
|
|
func buildPackage(ctx BuildContext) Package {
|
|
return Package{
|
|
Metadata: BuildMetadata(ctx),
|
|
CurrentConditions: currentConditions(ctx.Bundle),
|
|
}
|
|
}
|
|
|
|
func copyLocation(location *LocationContext) *LocationContext {
|
|
if location == nil {
|
|
return nil
|
|
}
|
|
copied := *location
|
|
return &copied
|
|
}
|
|
|
|
func currentConditions(bundle *forecast.Bundle) *CurrentConditionsContext {
|
|
if bundle == nil || bundle.Current == nil {
|
|
return nil
|
|
}
|
|
current := bundle.Current
|
|
context := CurrentConditionsContext{
|
|
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 context.ConditionText == "" &&
|
|
context.IsDay == nil &&
|
|
context.TemperatureC == nil &&
|
|
context.TemperatureF == nil &&
|
|
context.ApparentTemperatureC == nil &&
|
|
context.ApparentTemperatureF == nil &&
|
|
context.DewpointC == nil &&
|
|
context.DewpointF == nil &&
|
|
context.RelativeHumidityPercent == nil &&
|
|
context.WindSpeedKmh == nil &&
|
|
context.WindSpeedMph == nil &&
|
|
context.WindDirectionDegrees == nil {
|
|
return nil
|
|
}
|
|
return &context
|
|
}
|
|
|
|
func copyBool(value *bool) *bool {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
copied := *value
|
|
return &copied
|
|
}
|
|
|
|
func copyFloat(value *float64) *float64 {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
copied := *value
|
|
return &copied
|
|
}
|
|
|
|
func Save(path string, pkg Package) error {
|
|
if err := fileutil.WriteJSONAtomic(path, pkg); err != nil {
|
|
return fmt.Errorf("save briefing package: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sourceLocation(bundle *forecast.Bundle) (string, string) {
|
|
if bundle == nil {
|
|
return "", ""
|
|
}
|
|
for _, run := range []*forecast.ForecastRun{bundle.Hourly, bundle.Narrative, bundle.Daily} {
|
|
if run == nil {
|
|
continue
|
|
}
|
|
if run.LocationID != "" || run.LocationName != "" {
|
|
return run.LocationID, run.LocationName
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func sourceMetadata(bundle *forecast.Bundle) []SourceMetadata {
|
|
if bundle == nil {
|
|
return nil
|
|
}
|
|
out := make([]SourceMetadata, 0, len(bundle.Sources))
|
|
for _, source := range bundle.Sources {
|
|
out = append(out, SourceMetadata{
|
|
Name: source.Name,
|
|
Endpoint: source.Endpoint,
|
|
FetchedAt: source.FetchedAt,
|
|
IssuedAt: source.IssuedAt,
|
|
UpdatedAt: source.UpdatedAt,
|
|
DataSHA256: source.DataSHA256,
|
|
Missing: source.Missing,
|
|
Warnings: source.Warnings,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sourceWarnings(bundle *forecast.Bundle) []forecast.SourceWarning {
|
|
if bundle == nil {
|
|
return nil
|
|
}
|
|
return bundle.Warnings
|
|
}
|
|
|
|
func alertStatus(bundle *forecast.Bundle) *AlertStatus {
|
|
if bundle == nil {
|
|
return nil
|
|
}
|
|
status := &AlertStatus{}
|
|
if bundle.Alerts != nil {
|
|
status.Checked = true
|
|
status.ActiveCount = len(bundle.Alerts.Alerts)
|
|
}
|
|
for _, source := range bundle.Sources {
|
|
if source.Name == "alerts" && source.Missing {
|
|
status.Missing = true
|
|
break
|
|
}
|
|
}
|
|
if !status.Checked && !status.Missing {
|
|
return nil
|
|
}
|
|
return status
|
|
}
|
|
|
|
func setRelevantAlertCount(metadata *Metadata, count int) {
|
|
if metadata.Alerts == nil {
|
|
if count == 0 {
|
|
return
|
|
}
|
|
metadata.Alerts = &AlertStatus{}
|
|
}
|
|
metadata.Alerts.RelevantCount = count
|
|
}
|
|
|
|
func variantForReport(id report.ID) string {
|
|
switch id {
|
|
case report.DailyToday:
|
|
return "today"
|
|
case report.DailyTomorrow:
|
|
return "tomorrow"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|