Add a /conditions/current endpoint
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -36,6 +36,51 @@ FROM observations
|
||||
ORDER BY observed_at DESC, event_emitted_at DESC
|
||||
LIMIT 1`
|
||||
|
||||
queryCurrentConditions = `
|
||||
WITH windowed AS (
|
||||
SELECT
|
||||
temperature_c,
|
||||
apparent_temperature_c,
|
||||
dewpoint_c,
|
||||
relative_humidity_percent,
|
||||
wind_speed_kmh,
|
||||
wind_direction_degrees,
|
||||
condition_code,
|
||||
is_day,
|
||||
observed_at
|
||||
FROM observations
|
||||
WHERE observed_at > CURRENT_TIMESTAMP - make_interval(mins => $1)
|
||||
)
|
||||
SELECT
|
||||
COUNT(*) AS sample_count,
|
||||
AVG(temperature_c) AS temperature_c,
|
||||
AVG(apparent_temperature_c) AS apparent_temperature_c,
|
||||
AVG(dewpoint_c) AS dewpoint_c,
|
||||
AVG(relative_humidity_percent) AS relative_humidity_percent,
|
||||
AVG(wind_speed_kmh) AS wind_speed_kmh,
|
||||
CASE
|
||||
WHEN atan2d(
|
||||
AVG(sind(wind_direction_degrees)),
|
||||
AVG(cosd(wind_direction_degrees))
|
||||
) < 0
|
||||
THEN atan2d(
|
||||
AVG(sind(wind_direction_degrees)),
|
||||
AVG(cosd(wind_direction_degrees))
|
||||
) + 360.0
|
||||
ELSE atan2d(
|
||||
AVG(sind(wind_direction_degrees)),
|
||||
AVG(cosd(wind_direction_degrees))
|
||||
)
|
||||
END AS wind_direction_degrees,
|
||||
MAX(condition_code) AS condition_code,
|
||||
(
|
||||
SELECT is_day
|
||||
FROM windowed
|
||||
ORDER BY observed_at DESC
|
||||
LIMIT 1
|
||||
) AS is_day
|
||||
FROM windowed`
|
||||
|
||||
queryObservationPresentWeather = `
|
||||
SELECT weather_index, raw_text
|
||||
FROM observation_present_weather
|
||||
@@ -193,6 +238,33 @@ func (r *Repository) LatestObservation(ctx context.Context) (*model.WeatherObser
|
||||
return &obs, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CurrentConditions(ctx context.Context, observationWindowMinutes int) (*core.CurrentConditions, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, fmt.Errorf("postgres repository is not configured")
|
||||
}
|
||||
|
||||
var row currentConditionsRow
|
||||
err := r.db.QueryRowContext(ctx, queryCurrentConditions, observationWindowMinutes).Scan(
|
||||
&row.SampleCount,
|
||||
&row.TemperatureC,
|
||||
&row.ApparentTemperatureC,
|
||||
&row.DewpointC,
|
||||
&row.RelativeHumidityPercent,
|
||||
&row.WindSpeedKmh,
|
||||
&row.WindDirectionDegrees,
|
||||
&row.ConditionCode,
|
||||
&row.IsDay,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query current conditions: %w", err)
|
||||
}
|
||||
|
||||
return mapCurrentConditionsRow(row), nil
|
||||
}
|
||||
|
||||
func (r *Repository) LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, fmt.Errorf("postgres repository is not configured")
|
||||
@@ -419,6 +491,40 @@ type observationParentRow struct {
|
||||
ApparentTemperatureC sql.NullFloat64
|
||||
}
|
||||
|
||||
type currentConditionsRow struct {
|
||||
SampleCount int64
|
||||
TemperatureC sql.NullFloat64
|
||||
ApparentTemperatureC sql.NullFloat64
|
||||
DewpointC sql.NullFloat64
|
||||
RelativeHumidityPercent sql.NullFloat64
|
||||
WindSpeedKmh sql.NullFloat64
|
||||
WindDirectionDegrees sql.NullFloat64
|
||||
ConditionCode sql.NullInt64
|
||||
IsDay sql.NullBool
|
||||
}
|
||||
|
||||
func mapCurrentConditionsRow(row currentConditionsRow) *core.CurrentConditions {
|
||||
if row.SampleCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
conditionCode := model.WMOUnknown
|
||||
if row.ConditionCode.Valid {
|
||||
conditionCode = model.WMOCode(row.ConditionCode.Int64)
|
||||
}
|
||||
|
||||
return &core.CurrentConditions{
|
||||
TemperatureC: float64Ptr(row.TemperatureC),
|
||||
ApparentTemperatureC: float64Ptr(row.ApparentTemperatureC),
|
||||
DewpointC: float64Ptr(row.DewpointC),
|
||||
RelativeHumidityPercent: float64Ptr(row.RelativeHumidityPercent),
|
||||
WindSpeedKmh: float64Ptr(row.WindSpeedKmh),
|
||||
WindDirectionDegrees: float64Ptr(row.WindDirectionDegrees),
|
||||
ConditionCode: conditionCode,
|
||||
IsDay: boolPtr(row.IsDay),
|
||||
}
|
||||
}
|
||||
|
||||
func mapObservationParentRow(row observationParentRow) model.WeatherObservation {
|
||||
return model.WeatherObservation{
|
||||
StationID: stringValue(row.StationID),
|
||||
|
||||
@@ -110,3 +110,54 @@ func TestAttachAlertReferencesPreservesOrder(t *testing.T) {
|
||||
t.Fatalf("unexpected second alert references: %+v", out[1].References)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapCurrentConditionsRowNoSamplesReturnsNil(t *testing.T) {
|
||||
got := mapCurrentConditionsRow(currentConditionsRow{
|
||||
SampleCount: 0,
|
||||
})
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil for empty sample window, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapCurrentConditionsRowMapsFields(t *testing.T) {
|
||||
isDay := true
|
||||
got := mapCurrentConditionsRow(currentConditionsRow{
|
||||
SampleCount: 12,
|
||||
TemperatureC: sql.NullFloat64{Float64: 15.5, Valid: true},
|
||||
ApparentTemperatureC: sql.NullFloat64{Float64: 14.2, Valid: true},
|
||||
DewpointC: sql.NullFloat64{Float64: 10.1, Valid: true},
|
||||
RelativeHumidityPercent: sql.NullFloat64{Float64: 72, Valid: true},
|
||||
WindSpeedKmh: sql.NullFloat64{Float64: 24.8, Valid: true},
|
||||
WindDirectionDegrees: sql.NullFloat64{Float64: 182.5, Valid: true},
|
||||
ConditionCode: sql.NullInt64{Int64: 65, Valid: true},
|
||||
IsDay: sql.NullBool{Bool: isDay, Valid: true},
|
||||
})
|
||||
if got == nil {
|
||||
t.Fatalf("expected mapped current conditions")
|
||||
}
|
||||
if got.TemperatureC == nil || *got.TemperatureC != 15.5 {
|
||||
t.Fatalf("expected temperature pointer 15.5, got %v", got.TemperatureC)
|
||||
}
|
||||
if got.ApparentTemperatureC == nil || *got.ApparentTemperatureC != 14.2 {
|
||||
t.Fatalf("expected apparent temp pointer 14.2, got %v", got.ApparentTemperatureC)
|
||||
}
|
||||
if got.DewpointC == nil || *got.DewpointC != 10.1 {
|
||||
t.Fatalf("expected dewpoint pointer 10.1, got %v", got.DewpointC)
|
||||
}
|
||||
if got.RelativeHumidityPercent == nil || *got.RelativeHumidityPercent != 72 {
|
||||
t.Fatalf("expected rh pointer 72, got %v", got.RelativeHumidityPercent)
|
||||
}
|
||||
if got.WindSpeedKmh == nil || *got.WindSpeedKmh != 24.8 {
|
||||
t.Fatalf("expected wind speed pointer 24.8, got %v", got.WindSpeedKmh)
|
||||
}
|
||||
if got.WindDirectionDegrees == nil || *got.WindDirectionDegrees != 182.5 {
|
||||
t.Fatalf("expected wind direction pointer 182.5, got %v", got.WindDirectionDegrees)
|
||||
}
|
||||
if got.ConditionCode != 65 {
|
||||
t.Fatalf("expected condition code 65, got %d", got.ConditionCode)
|
||||
}
|
||||
if got.IsDay == nil || !*got.IsDay {
|
||||
t.Fatalf("expected isDay pointer true, got %v", got.IsDay)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user