diff --git a/internal/model/forecast.go b/internal/model/forecast.go index b48cd26..1580c6c 100644 --- a/internal/model/forecast.go +++ b/internal/model/forecast.go @@ -103,8 +103,6 @@ type WeatherForecastPeriod struct { BarometricPressurePa *float64 `json:"barometricPressurePa,omitempty"` VisibilityMeters *float64 `json:"visibilityMeters,omitempty"` ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"` - WindChillC *float64 `json:"windChillC,omitempty"` - HeatIndexC *float64 `json:"heatIndexC,omitempty"` CloudCoverPercent *float64 `json:"cloudCoverPercent,omitempty"` // Precipitation (forecast-specific). Keep these generic and provider-independent. diff --git a/internal/model/observation.go b/internal/model/observation.go index 5a0b621..f99c004 100644 --- a/internal/model/observation.go +++ b/internal/model/observation.go @@ -36,8 +36,7 @@ type WeatherObservation struct { VisibilityMeters *float64 `json:"visibilityMeters,omitempty"` RelativeHumidityPercent *float64 `json:"relativeHumidityPercent,omitempty"` - WindChillC *float64 `json:"windChillC,omitempty"` - HeatIndexC *float64 `json:"heatIndexC,omitempty"` + ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"` ElevationMeters *float64 `json:"elevationMeters,omitempty"` RawMessage string `json:"rawMessage,omitempty"` diff --git a/internal/normalizers/nws/observation.go b/internal/normalizers/nws/observation.go index 7a1ea16..cb004a2 100644 --- a/internal/normalizers/nws/observation.go +++ b/internal/normalizers/nws/observation.go @@ -80,6 +80,14 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation, // NWS observation responses typically do not include a day/night flag -> nil. canonicalText := standards.WMOText(wmo, nil) + // Apparent temperature: prefer wind chill when both are supplied. + var apparentC *float64 + if parsed.Properties.WindChill.Value != nil { + apparentC = parsed.Properties.WindChill.Value + } else if parsed.Properties.HeatIndex.Value != nil { + apparentC = parsed.Properties.HeatIndex.Value + } + obs := model.WeatherObservation{ StationID: parsed.Properties.StationID, StationName: parsed.Properties.StationName, @@ -108,8 +116,7 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation, VisibilityMeters: parsed.Properties.Visibility.Value, RelativeHumidityPercent: parsed.Properties.RelativeHumidity.Value, - WindChillC: parsed.Properties.WindChill.Value, - HeatIndexC: parsed.Properties.HeatIndex.Value, + ApparentTemperatureC: apparentC, ElevationMeters: parsed.Properties.Elevation.Value, RawMessage: parsed.Properties.RawMessage, diff --git a/internal/normalizers/openmeteo/forecast.go b/internal/normalizers/openmeteo/forecast.go index bc7bdbf..71cd6de 100644 --- a/internal/normalizers/openmeteo/forecast.go +++ b/internal/normalizers/openmeteo/forecast.go @@ -25,7 +25,7 @@ import ( // Event.EmittedAt when present, otherwise the first hourly time. // - Hourly payloads are array-oriented; missing fields are treated as nil per-period. // - Snowfall is provided in centimeters and is converted to millimeters. -// - apparent_temperature is ignored (no canonical "feels like" field). +// - apparent_temperature is mapped to ApparentTemperatureC when present. type ForecastNormalizer struct{} func (ForecastNormalizer) Match(e event.Event) bool { @@ -121,6 +121,9 @@ func buildForecast(parsed omForecastResponse, fallbackIssued time.Time) (model.W if v := floatAt(parsed.Hourly.Temperature2m, i); v != nil { period.TemperatureC = v } + if v := floatAt(parsed.Hourly.ApparentTemp, i); v != nil { + period.ApparentTemperatureC = v + } if v := floatAt(parsed.Hourly.DewPoint2m, i); v != nil { period.DewpointC = v } diff --git a/internal/normalizers/openmeteo/observation.go b/internal/normalizers/openmeteo/observation.go index 1553e7a..e7679b5 100644 --- a/internal/normalizers/openmeteo/observation.go +++ b/internal/normalizers/openmeteo/observation.go @@ -107,6 +107,10 @@ func buildObservation(parsed omResponse) (model.WeatherObservation, time.Time, e v := *parsed.Current.Temperature2m obs.TemperatureC = &v } + if parsed.Current.ApparentTemperature != nil { + v := *parsed.Current.ApparentTemperature + obs.ApparentTemperatureC = &v + } if parsed.Current.RelativeHumidity2m != nil { v := *parsed.Current.RelativeHumidity2m diff --git a/internal/normalizers/openmeteo/types.go b/internal/normalizers/openmeteo/types.go index 5e25463..3c56145 100644 --- a/internal/normalizers/openmeteo/types.go +++ b/internal/normalizers/openmeteo/types.go @@ -18,9 +18,10 @@ type omResponse struct { type omCurrent struct { Time string `json:"time"` // e.g. "2026-01-10T12:30" (often no timezone suffix) - Temperature2m *float64 `json:"temperature_2m"` - RelativeHumidity2m *float64 `json:"relative_humidity_2m"` - WeatherCode *int `json:"weather_code"` + Temperature2m *float64 `json:"temperature_2m"` + ApparentTemperature *float64 `json:"apparent_temperature"` + RelativeHumidity2m *float64 `json:"relative_humidity_2m"` + WeatherCode *int `json:"weather_code"` WindSpeed10m *float64 `json:"wind_speed_10m"` // km/h (per Open-Meteo docs for these fields) WindDirection10m *float64 `json:"wind_direction_10m"` // degrees diff --git a/internal/normalizers/openweather/observation.go b/internal/normalizers/openweather/observation.go index 17d2cf1..98bfc2e 100644 --- a/internal/normalizers/openweather/observation.go +++ b/internal/normalizers/openweather/observation.go @@ -68,6 +68,11 @@ func buildObservation(parsed owmResponse) (model.WeatherObservation, time.Time, // - wind speed is m/s -> km/h conversion tempC := parsed.Main.Temp rh := parsed.Main.Humidity + var apparentC *float64 + if parsed.Main.FeelsLike != nil { + v := *parsed.Main.FeelsLike + apparentC = &v + } surfacePa := normcommon.PressurePaFromHPa(parsed.Main.Pressure) var seaLevelPa *float64 @@ -117,6 +122,7 @@ func buildObservation(parsed owmResponse) (model.WeatherObservation, time.Time, IconURL: iconURL, TemperatureC: &tempC, + ApparentTemperatureC: apparentC, WindDirectionDegrees: parsed.Wind.Deg, WindSpeedKmh: &wsKmh, diff --git a/internal/normalizers/openweather/types.go b/internal/normalizers/openweather/types.go index ce76a5f..df2a09d 100644 --- a/internal/normalizers/openweather/types.go +++ b/internal/normalizers/openweather/types.go @@ -15,10 +15,11 @@ type owmResponse struct { Weather []owmWeather `json:"weather"` Main struct { - Temp float64 `json:"temp"` // °C when units=metric (enforced by source) - Pressure float64 `json:"pressure"` // hPa - Humidity float64 `json:"humidity"` // % - SeaLevel *float64 `json:"sea_level"` + Temp float64 `json:"temp"` // °C when units=metric (enforced by source) + FeelsLike *float64 `json:"feels_like"` // °C when units=metric (enforced by source) + Pressure float64 `json:"pressure"` // hPa + Humidity float64 `json:"humidity"` // % + SeaLevel *float64 `json:"sea_level"` } `json:"main"` Visibility *float64 `json:"visibility"` // meters (optional)