package openmeteo import ( "context" "encoding/json" "strings" "time" "gitea.maximumdirect.net/ejr/feedkit/config" "gitea.maximumdirect.net/ejr/feedkit/event" "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/openmeteo" "gitea.maximumdirect.net/ejr/weatherfeeder/internal/sources/common" "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) // ForecastSource polls an Open-Meteo hourly forecast endpoint and emits one RAW Forecast Event. type ForecastSource struct { http *common.HTTPSource } func NewForecastSource(cfg config.SourceConfig) (*ForecastSource, error) { const driver = "openmeteo_forecast" hs, err := common.NewHTTPSource(driver, cfg, "application/json") if err != nil { return nil, err } return &ForecastSource{http: hs}, nil } func (s *ForecastSource) Name() string { return s.http.Name } func (s *ForecastSource) Kind() event.Kind { return event.Kind("forecast") } func (s *ForecastSource) Poll(ctx context.Context) ([]event.Event, error) { raw, meta, err := s.fetchRaw(ctx) if err != nil { return nil, err } // Open-Meteo does not expose a true "issued at" timestamp for forecast runs. // We use current.time when present; otherwise we fall back to the first hourly time // as a proxy for the start of the forecast horizon. var effectiveAt *time.Time if !meta.ParsedTimestamp.IsZero() { t := meta.ParsedTimestamp.UTC() effectiveAt = &t } emittedAt := time.Now().UTC() eventID := common.ChooseEventID("", s.http.Name, effectiveAt, emittedAt) return common.SingleRawEvent( s.Kind(), s.http.Name, standards.SchemaRawOpenMeteoHourlyForecastV1, eventID, emittedAt, effectiveAt, raw, ) } // ---- RAW fetch + minimal metadata decode ---- type forecastMeta struct { Timezone string `json:"timezone"` UTCOffsetSeconds int `json:"utc_offset_seconds"` Current struct { Time string `json:"time"` } `json:"current"` Hourly struct { Time []string `json:"time"` } `json:"hourly"` ParsedTimestamp time.Time `json:"-"` } func (s *ForecastSource) fetchRaw(ctx context.Context) (json.RawMessage, forecastMeta, error) { raw, err := s.http.FetchJSON(ctx) if err != nil { return nil, forecastMeta{}, err } var meta forecastMeta if err := json.Unmarshal(raw, &meta); err != nil { // If metadata decode fails, still return raw; Poll will fall back to Source:EmittedAt. return raw, forecastMeta{}, nil } ts := strings.TrimSpace(meta.Current.Time) if ts == "" { for _, v := range meta.Hourly.Time { if ts = strings.TrimSpace(v); ts != "" { break } } } if ts != "" { if t, err := openmeteo.ParseTime(ts, meta.Timezone, meta.UTCOffsetSeconds); err == nil { meta.ParsedTimestamp = t.UTC() } } return raw, meta, nil }