Files
Maven/internal/weather/openmeteo.go
T
kami e030466cac maven: weather module skeleton with open-meteo provider (task 5)
- New internal/weather/ package: Provider interface, Weather struct, StubProvider
- OpenMeteoProvider with geocoding + current weather (keyless, free API)
- Config: WeatherConfig in VoiceConfig (provider, default_location)
- Wire in voice.go as weatherProvider on reactiveHandler
- Handle weather queries in IntentQuery (before notes RAG)
- Helper: isWeatherQuery / extractWeatherLocation
- Tests: mocked HTTP round-trip for OpenMeteo, stub ErrNotConfigured, config tests
- No real network calls in any test

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:15:54 +04:00

130 lines
3.0 KiB
Go

package weather
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
type OpenMeteoProvider struct {
httpClient *http.Client
}
func NewOpenMeteoProvider() *OpenMeteoProvider {
return &OpenMeteoProvider{
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
func (p *OpenMeteoProvider) SetClient(c *http.Client) {
p.httpClient = c
}
type geoResult struct {
Name string `json:"name"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Country string `json:"country"`
}
type geoResponse struct {
Results []geoResult `json:"results"`
}
type currentWeather struct {
Temperature float64 `json:"temperature"`
WMO int `json:"weathercode"`
WindSpeed float64 `json:"windspeed"`
}
type forecastResponse struct {
CurrentWeather currentWeather `json:"current_weather"`
}
var wmoCodes = map[int]string{
0: "ясно",
1: "преимущественно ясно",
2: "облачно",
3: "пасмурно",
45: "туман",
51: "морось",
61: "дождь",
71: "снег",
95: "гроза",
}
func (p *OpenMeteoProvider) CurrentWeather(ctx context.Context, location string) (Weather, error) {
lat, lon, name, err := p.geocode(ctx, location)
if err != nil {
return Weather{}, fmt.Errorf("open-meteo: geocode: %w", err)
}
u := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f&current_weather=true", lat, lon)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return Weather{}, fmt.Errorf("open-meteo: request: %w", err)
}
resp, err := p.httpClient.Do(req)
if err != nil {
return Weather{}, fmt.Errorf("open-meteo: http: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return Weather{}, fmt.Errorf("open-meteo: read: %w", err)
}
var forecast forecastResponse
if err := json.Unmarshal(body, &forecast); err != nil {
return Weather{}, fmt.Errorf("open-meteo: decode: %w", err)
}
condition := wmoCodes[forecast.CurrentWeather.WMO]
if condition == "" {
condition = "неизвестно"
}
return Weather{
Location: name,
Temperature: forecast.CurrentWeather.Temperature,
Condition: condition,
}, nil
}
func (p *OpenMeteoProvider) geocode(ctx context.Context, location string) (lat, lon float64, name string, err error) {
u := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=ru&format=json", url.QueryEscape(location))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return 0, 0, "", err
}
resp, err := p.httpClient.Do(req)
if err != nil {
return 0, 0, "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, 0, "", err
}
var geo geoResponse
if err := json.Unmarshal(body, &geo); err != nil {
return 0, 0, "", err
}
if len(geo.Results) == 0 {
return 0, 0, "", fmt.Errorf("location %q not found", location)
}
r := geo.Results[0]
return r.Latitude, r.Longitude, r.Name, nil
}