140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
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, we’ll 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
|
||
}
|