Added a /conditions/current endpoint with computed best-guess values for current conditions
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-03-17 15:51:00 -05:00
parent e3aab86376
commit cb316c228a
19 changed files with 1146 additions and 358 deletions

View File

@@ -7,72 +7,103 @@ import (
"gitea.maximumdirect.net/ejr/weatherapi/internal/application/units"
"gitea.maximumdirect.net/ejr/weatherapi/internal/core/ports"
"gitea.maximumdirect.net/ejr/weatherapi/internal/platform/constants"
)
type Service struct {
repo ports.ObservationRepository
converter units.Converter
window time.Duration
repo ports.ObservationRepository
registry *units.Registry
}
func NewService(repo ports.ObservationRepository, converter units.Converter, window time.Duration) *Service {
return &Service{repo: repo, converter: converter, window: window}
func NewService(repo ports.ObservationRepository, registry *units.Registry) *Service {
return &Service{repo: repo, registry: registry}
}
type CurrentResponse struct {
Summary SummaryResponse `json:"summary"`
Conditions []ConditionResponse `json:"conditions"`
PrecipitationEvents []string `json:"precipitationEvents"`
type Response struct {
Observations []ObservationResponse `json:"observations"`
}
type SummaryResponse struct {
TemperatureF *float64 `json:"temperatureF,omitempty"`
ApparentTemperatureF *float64 `json:"apparentTemperatureF,omitempty"`
type ObservationResponse struct {
StationID *string `json:"stationId,omitempty"`
StationName *string `json:"stationName,omitempty"`
Timestamp time.Time `json:"timestamp"`
ConditionCode int `json:"conditionCode"`
IsDay *bool `json:"isDay,omitempty"`
TextDescription *string `json:"textDescription,omitempty"`
TemperatureC *float64 `json:"temperatureC,omitempty"`
TemperatureF *float64 `json:"temperatureF,omitempty"`
DewpointC *float64 `json:"dewpointC,omitempty"`
DewpointF *float64 `json:"dewpointF,omitempty"`
WindDirectionDegrees *float64 `json:"windDirectionDegrees,omitempty"`
WindSpeedKmh *float64 `json:"windSpeedKmh,omitempty"`
WindSpeedMph *float64 `json:"windSpeedMph,omitempty"`
WindGustKmh *float64 `json:"windGustKmh,omitempty"`
WindGustMph *float64 `json:"windGustMph,omitempty"`
BarometricPressurePa *float64 `json:"barometricPressurePa,omitempty"`
VisibilityMeters *float64 `json:"visibilityMeters,omitempty"`
RelativeHumidityPercent *float64 `json:"relativeHumidityPercent,omitempty"`
ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"`
ApparentTemperatureF *float64 `json:"apparentTemperatureF,omitempty"`
PresentWeather []PresentWeatherResponse `json:"presentWeather,omitempty"`
}
type ConditionResponse struct {
StationID *string `json:"stationId,omitempty"`
ObservedAt time.Time `json:"observedAt"`
TemperatureF *float64 `json:"temperatureF,omitempty"`
TextDescription *string `json:"textDescription,omitempty"`
type PresentWeatherResponse struct {
Raw map[string]any `json:"raw,omitempty"`
}
func (s *Service) GetCurrent(ctx context.Context) (CurrentResponse, error) {
func (s *Service) GetRecent(ctx context.Context, count int, unitSystem string) (Response, error) {
if s == nil {
return CurrentResponse{}, fmt.Errorf("observations service is nil")
return Response{}, fmt.Errorf("observations service is nil")
}
summary, err := s.repo.GetCurrentSummary(ctx, s.window)
converter, err := s.registry.Resolve(unitSystem)
if err != nil {
return CurrentResponse{}, err
return Response{}, err
}
conditionsMetric, err := s.repo.ListCurrentConditions(ctx, s.window)
records, err := s.repo.ListRecentObservations(ctx, count)
if err != nil {
return CurrentResponse{}, err
return Response{}, err
}
precip, err := s.repo.ListCurrentPrecipitationEvents(ctx, s.window)
if err != nil {
return CurrentResponse{}, err
out := make([]ObservationResponse, 0, len(records))
for _, r := range records {
item := ObservationResponse{
StationID: r.StationID,
StationName: r.StationName,
Timestamp: r.Timestamp,
ConditionCode: r.ConditionCode,
IsDay: r.IsDay,
TextDescription: r.TextDescription,
WindDirectionDegrees: r.WindDirectionDegrees,
BarometricPressurePa: r.BarometricPressurePa,
VisibilityMeters: r.VisibilityMeters,
RelativeHumidityPercent: r.RelativeHumidityPercent,
}
presentWeather := make([]PresentWeatherResponse, 0, len(r.PresentWeather))
for _, pw := range r.PresentWeather {
presentWeather = append(presentWeather, PresentWeatherResponse{Raw: pw.Raw})
}
item.PresentWeather = presentWeather
switch converter.System() {
case constants.UnitSystemUS:
item.TemperatureF = converter.TemperatureCToOutput(r.TemperatureC)
item.DewpointF = converter.TemperatureCToOutput(r.DewpointC)
item.WindSpeedMph = converter.SpeedKmhToOutput(r.WindSpeedKmh)
item.WindGustMph = converter.SpeedKmhToOutput(r.WindGustKmh)
item.ApparentTemperatureF = converter.TemperatureCToOutput(r.ApparentTemperatureC)
default:
item.TemperatureC = converter.TemperatureCToOutput(r.TemperatureC)
item.DewpointC = converter.TemperatureCToOutput(r.DewpointC)
item.WindSpeedKmh = converter.SpeedKmhToOutput(r.WindSpeedKmh)
item.WindGustKmh = converter.SpeedKmhToOutput(r.WindGustKmh)
item.ApparentTemperatureC = converter.TemperatureCToOutput(r.ApparentTemperatureC)
}
out = append(out, item)
}
conditions := make([]ConditionResponse, 0, len(conditionsMetric))
for _, c := range conditionsMetric {
conditions = append(conditions, ConditionResponse{
StationID: c.StationID,
ObservedAt: c.ObservedAt,
TemperatureF: s.converter.TemperatureCToOutput(c.TemperatureC),
TextDescription: c.TextDescription,
})
}
return CurrentResponse{
Summary: SummaryResponse{
TemperatureF: s.converter.TemperatureCToOutput(summary.TemperatureC),
ApparentTemperatureF: s.converter.TemperatureCToOutput(summary.ApparentTemperatureC),
},
Conditions: conditions,
PrecipitationEvents: precip,
}, nil
return Response{Observations: out}, nil
}