This repository has been archived on 2025-11-19. You can view files and clone it, but cannot push or open issues or pull requests.
Skyfeed_archive/internal/weather/fetch.go

140 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package weather
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings" // ✅ Required for province normalization
"time"
"github.com/leaktechnologies/skyfeed/internal/config"
)
// WeatherData holds simplified normalized current weather data.
type WeatherData struct {
Station string `json:"station"`
Temperature float64 `json:"temperature"`
Condition string `json:"condition"`
Humidity string `json:"humidity"`
Pressure string `json:"pressure"`
WindSpeed string `json:"wind_speed"`
WindDir string `json:"wind_dir"`
Timestamp string `json:"timestamp"`
}
// Supported province folders in Environment Canada citypage_weather XML structure.
var provinceCodes = []string{
"AB", "BC", "MB", "NB", "NL", "NS", "NT", "NU",
"ON", "PE", "QC", "SK", "YT",
}
// FetchCurrent retrieves current weather from Environment Canada for any province.
func FetchCurrent(stationCode, province string) (WeatherData, error) {
if stationCode == "" {
return WeatherData{}, fmt.Errorf("no station code provided")
}
// If province unknown, well probe each possible province directory until one succeeds.
targetProvinces := provinceCodes
if province != "" {
targetProvinces = []string{strings.ToUpper(province)}
}
var lastErr error
for _, prov := range targetProvinces {
url := fmt.Sprintf("https://dd.weather.gc.ca/citypage_weather/xml/%s/%s_e.xml", prov, stationCode)
fmt.Printf("[weather] Fetching current weather for %s in %s...\n", stationCode, prov)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(url)
if err != nil {
lastErr = err
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
lastErr = fmt.Errorf("HTTP %s", resp.Status)
continue
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return WeatherData{}, fmt.Errorf("failed to read EC XML: %w", err)
}
var parsed struct {
XMLName xml.Name `xml:"siteData"`
Location string `xml:"location>name"`
CurrentConditions struct {
Temperature string `xml:"temperature"`
Condition string `xml:"condition"`
RelativeHumidity string `xml:"relativeHumidity"`
Pressure string `xml:"pressure"`
Wind struct {
Speed string `xml:"speed"`
Direction string `xml:"direction"`
} `xml:"wind"`
} `xml:"currentConditions"`
}
if err := xml.Unmarshal(body, &parsed); err != nil {
lastErr = fmt.Errorf("failed to parse EC XML: %w", err)
continue
}
if parsed.CurrentConditions.Temperature == "" && parsed.CurrentConditions.Condition == "" {
lastErr = fmt.Errorf("no data for %s in %s", stationCode, prov)
continue
}
temp, _ := strconv.ParseFloat(parsed.CurrentConditions.Temperature, 64)
data := WeatherData{
Station: parsed.Location,
Temperature: temp,
Condition: NormalizeCondition(parsed.CurrentConditions.Condition),
Humidity: parsed.CurrentConditions.RelativeHumidity,
Pressure: parsed.CurrentConditions.Pressure,
WindSpeed: parsed.CurrentConditions.Wind.Speed,
WindDir: parsed.CurrentConditions.Wind.Direction,
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
if err := SaveToCache(data); err != nil {
fmt.Println("[weather] Warning: failed to save cache:", err)
}
fmt.Printf("[SUCCESS] Current: %.1f°C, %s (%s)\n", data.Temperature, data.Condition, prov)
return data, nil
}
if lastErr != nil {
return WeatherData{}, fmt.Errorf("no valid feed found for %s: %v", stationCode, lastErr)
}
return WeatherData{}, fmt.Errorf("failed to fetch weather for %s", stationCode)
}
// LoadCached loads the last weather data from disk for offline fallback.
func LoadCached() (WeatherData, error) {
cachePath := filepath.Join(config.DataDir, "last_weather.json")
file, err := os.Open(cachePath)
if err != nil {
return WeatherData{}, fmt.Errorf("no cached weather found")
}
defer file.Close()
var data WeatherData
if err := json.NewDecoder(file).Decode(&data); err != nil {
return WeatherData{}, fmt.Errorf("failed to decode cache: %w", err)
}
fmt.Println("[weather] Loaded cached weather →", data.Timestamp)
return data, nil
}