mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-18 21:44:08 +01:00
155 lines
5.2 KiB
Go
155 lines
5.2 KiB
Go
// backend/internal/services/weather/openweathermap_fetcher.go
|
|
package weather
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/forfarm/backend/internal/domain"
|
|
)
|
|
|
|
const openWeatherMapOneCallAPIURL = "https://api.openweathermap.org/data/3.0/onecall"
|
|
|
|
type openWeatherMapOneCallResponse struct {
|
|
Lat float64 `json:"lat"`
|
|
Lon float64 `json:"lon"`
|
|
Timezone string `json:"timezone"`
|
|
TimezoneOffset int `json:"timezone_offset"`
|
|
Current *struct {
|
|
Dt int64 `json:"dt"` // Current time, Unix, UTC
|
|
Sunrise int64 `json:"sunrise"`
|
|
Sunset int64 `json:"sunset"`
|
|
Temp float64 `json:"temp"` // Kelvin by default, 'units=metric' for Celsius
|
|
FeelsLike float64 `json:"feels_like"` // Kelvin by default
|
|
Pressure int `json:"pressure"` // hPa
|
|
Humidity int `json:"humidity"` // %
|
|
DewPoint float64 `json:"dew_point"`
|
|
Uvi float64 `json:"uvi"`
|
|
Clouds int `json:"clouds"` // %
|
|
Visibility int `json:"visibility"` // meters
|
|
WindSpeed float64 `json:"wind_speed"` // meter/sec by default
|
|
WindDeg int `json:"wind_deg"`
|
|
WindGust float64 `json:"wind_gust,omitempty"`
|
|
Rain *struct {
|
|
OneH float64 `json:"1h"` // Rain volume for the last 1 hour, mm
|
|
} `json:"rain,omitempty"`
|
|
Snow *struct {
|
|
OneH float64 `json:"1h"` // Snow volume for the last 1 hour, mm
|
|
} `json:"snow,omitempty"`
|
|
Weather []struct {
|
|
ID int `json:"id"`
|
|
Main string `json:"main"`
|
|
Description string `json:"description"`
|
|
Icon string `json:"icon"`
|
|
} `json:"weather"`
|
|
} `json:"current,omitempty"`
|
|
// Minutely []...
|
|
// Hourly []...
|
|
// Daily []...
|
|
// Alerts []...
|
|
}
|
|
|
|
type OpenWeatherMapFetcher struct {
|
|
apiKey string
|
|
client *http.Client
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewOpenWeatherMapFetcher(apiKey string, client *http.Client, logger *slog.Logger) domain.WeatherFetcher {
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 10 * time.Second}
|
|
}
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &OpenWeatherMapFetcher{
|
|
apiKey: apiKey,
|
|
client: client,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (f *OpenWeatherMapFetcher) GetCurrentWeatherByCoords(ctx context.Context, lat, lon float64) (*domain.WeatherData, error) {
|
|
queryParams := url.Values{}
|
|
queryParams.Set("lat", fmt.Sprintf("%.4f", lat))
|
|
queryParams.Set("lon", fmt.Sprintf("%.4f", lon))
|
|
queryParams.Set("appid", f.apiKey)
|
|
queryParams.Set("units", "metric") // Request Celsius and m/s
|
|
queryParams.Set("exclude", "minutely,hourly,daily,alerts") // Exclude parts we don't need now
|
|
|
|
fullURL := fmt.Sprintf("%s?%s", openWeatherMapOneCallAPIURL, queryParams.Encode())
|
|
f.logger.Debug("Fetching weather from OpenWeatherMap OneCall API", "url", fullURL)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
|
|
if err != nil {
|
|
f.logger.Error("Failed to create OpenWeatherMap request", "error", err)
|
|
return nil, fmt.Errorf("failed to create weather request: %w", err)
|
|
}
|
|
|
|
resp, err := f.client.Do(req)
|
|
if err != nil {
|
|
f.logger.Error("Failed to execute OpenWeatherMap request", "url", fullURL, "error", err)
|
|
return nil, fmt.Errorf("failed to fetch weather data: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
// TODO: Read resp.Body to get error message from OpenWeatherMap
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
f.logger.Error("OpenWeatherMap API returned non-OK status",
|
|
"url", fullURL,
|
|
"status_code", resp.StatusCode,
|
|
"body", string(bodyBytes))
|
|
return nil, fmt.Errorf("weather API request failed with status: %s", resp.Status)
|
|
}
|
|
|
|
var owmResp openWeatherMapOneCallResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&owmResp); err != nil {
|
|
f.logger.Error("Failed to decode OpenWeatherMap OneCall response", "error", err)
|
|
return nil, fmt.Errorf("failed to decode weather response: %w", err)
|
|
}
|
|
|
|
if owmResp.Current == nil {
|
|
f.logger.Warn("OpenWeatherMap OneCall response missing 'current' weather data", "lat", lat, "lon", lon)
|
|
return nil, fmt.Errorf("current weather data not found in API response")
|
|
}
|
|
current := owmResp.Current
|
|
|
|
if len(current.Weather) == 0 {
|
|
f.logger.Warn("OpenWeatherMap response missing weather description details", "lat", lat, "lon", lon)
|
|
return nil, fmt.Errorf("weather data description not found in response")
|
|
}
|
|
|
|
// Create domain object using pointers for optional fields
|
|
weatherData := &domain.WeatherData{} // Initialize empty struct first
|
|
|
|
// Assign values using pointers, checking for nil where appropriate
|
|
weatherData.TempCelsius = ¤t.Temp
|
|
humidityFloat := float64(current.Humidity)
|
|
weatherData.Humidity = &humidityFloat
|
|
weatherData.Description = ¤t.Weather[0].Description
|
|
weatherData.Icon = ¤t.Weather[0].Icon
|
|
weatherData.WindSpeed = ¤t.WindSpeed
|
|
if current.Rain != nil {
|
|
weatherData.RainVolume1h = ¤t.Rain.OneH
|
|
}
|
|
observedTime := time.Unix(current.Dt, 0).UTC()
|
|
weatherData.ObservedAt = &observedTime
|
|
now := time.Now().UTC()
|
|
weatherData.WeatherLastUpdated = &now
|
|
|
|
f.logger.Debug("Successfully fetched weather data",
|
|
"lat", lat,
|
|
"lon", lon,
|
|
"temp", *weatherData.TempCelsius,
|
|
"description", *weatherData.Description)
|
|
|
|
return weatherData, nil
|
|
}
|