summaryrefslogtreecommitdiff
path: root/weather
diff options
context:
space:
mode:
Diffstat (limited to 'weather')
-rw-r--r--weather/weather.go193
1 files changed, 146 insertions, 47 deletions
diff --git a/weather/weather.go b/weather/weather.go
index 2a1d23f..0aa6cbb 100644
--- a/weather/weather.go
+++ b/weather/weather.go
@@ -2,67 +2,94 @@
package weather
import (
- "encoding/xml"
- "github.com/silvasur/startpage/http_getter"
- "github.com/silvasur/startpage/interval"
+ "encoding/json"
+ "fmt"
"log"
+ "net/http"
+ "strings"
"time"
+
+ "github.com/silvasur/startpage/http_getter"
+ "github.com/silvasur/startpage/interval"
)
-func toTime(s string) time.Time {
- t, _ := time.Parse("2006-01-02T15:04:05", s)
- return t
+type TimeseriesEntry struct {
+ Time time.Time `json:"time"`
+ Data struct {
+ Instant struct {
+ Details struct {
+ AirTemperature float64 `json:"air_temperature"`
+ } `json:"details"`
+ } `json:"instant"`
+ Next1Hours struct {
+ Summary struct {
+ SymbolCode string `json:"symbol_code"`
+ } `json:"summary"`
+ } `json:"next_1_hours"`
+ } `json:"data"`
}
-type Temperature struct {
- Value int `xml:"value,attr"`
- Unit string `xml:"unit,attr"`
+func (te TimeseriesEntry) Temperature() float64 {
+ return te.Data.Instant.Details.AirTemperature
}
-type Weather struct {
- Temp Temperature `xml:"temperature"`
- Symbol struct {
- Var string `xml:"var,attr"`
- } `xml:"symbol"`
- From string `xml:"from,attr"`
- URL string
- Icon string
+func (te TimeseriesEntry) SymbolCode() string {
+ return te.Data.Next1Hours.Summary.SymbolCode
}
-func (w *Weather) prepIcon() {
- w.Icon = "http://symbol.yr.no/grafikk/sym/b100/" + w.Symbol.Var + ".png"
-}
+type Timeseries []TimeseriesEntry
-type weatherdata struct {
- Forecast []*Weather `xml:"forecast>tabular>time"`
-}
+func (ts Timeseries) Current(now time.Time) *TimeseriesEntry {
+ if ts == nil {
+ return nil
+ }
-func CurrentWeather(place string) (*Weather, error) {
- url := "http://www.yr.no/place/" + place + "/forecast_hour_by_hour.xml"
+ found := false
+ var out TimeseriesEntry
- resp, err := http_getter.Get(url)
- if err != nil {
- return nil, err
+ for _, entry := range ts {
+ if entry.Time.After(now) {
+ // Is in the future, not interested
+ continue
+ }
+
+ if !found || now.Sub(out.Time) < now.Sub(entry.Time) {
+ out = entry
+ found = true
+ }
}
- defer resp.Body.Close()
- var wd weatherdata
- dec := xml.NewDecoder(resp.Body)
- if err := dec.Decode(&wd); err != nil {
- return nil, err
+ if found {
+ return &out
+ } else {
+ return nil
}
+}
- w := wd.Forecast[0]
- w.URL = "http://www.yr.no/place/" + place
- w.prepIcon()
+type ApiResponse struct {
+ Properties struct {
+ Timeseries Timeseries `json:"timeseries"`
+ } `json:"properties"`
+}
- return w, nil
+func truncateCoord(coord string) string {
+ parts := strings.SplitN(coord, ".", 2)
+ if len(parts) == 1 {
+ return parts[0] + ".0"
+ }
+ tail := parts[1]
+ if len(tail) > 4 {
+ tail = tail[0:4]
+ }
+ return parts[0] + "." + tail
}
type WeatherProvider struct {
- place string
+ lat, lon string
intervalRunner *interval.IntervalRunner
- weather *Weather
+ timeseries Timeseries
+ expires time.Time
+ lastModified time.Time
err error
}
@@ -71,26 +98,98 @@ const (
RETRY_INTERVAL = 1 * time.Minute
)
-func NewWeatherProvider(place string) *WeatherProvider {
+func NewWeatherProvider(lat, lon string) *WeatherProvider {
return &WeatherProvider{
- place: place,
+ lat: lat,
+ lon: lon,
intervalRunner: interval.NewIntervalRunner(UPDATE_INTERVAL, RETRY_INTERVAL),
}
}
-func (wp *WeatherProvider) CurrentWeather() (*Weather, error) {
+func parseTimeFromHeader(header http.Header, key string) (*time.Time, error) {
+ raw := header.Get(key)
+ if raw == "" {
+ return nil, fmt.Errorf("Could not parse time from header %s: Not set or empty", key)
+ }
+
+ t, err := http.ParseTime(raw)
+ if err != nil {
+ return nil, fmt.Errorf("Could not parse time from header %s: %w", key, err)
+ }
+
+ return &t, nil
+}
+
+func updateTimeFromHeaderIfOK(header http.Header, key string, time *time.Time) {
+ newTime, err := parseTimeFromHeader(header, key)
+ if err != nil {
+ log.Printf("Will not update time for key=%s: %s", key, err)
+ return
+ }
+
+ *time = *newTime
+}
+
+func (wp *WeatherProvider) update() error {
+ if time.Now().Before(wp.expires) {
+ log.Printf("Will not update weather yet, as it's not yet expired (expires=%s)", wp.expires)
+ return nil
+ }
+
+ url := "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=" + truncateCoord(wp.lat) + "&lon=" + truncateCoord(wp.lon)
+
+ req, err := http_getter.BuildGetRequest(url)
+ if err != nil {
+ return err
+ }
+
+ if wp.timeseries != nil {
+ req.Header.Add("If-Modified-Since", wp.lastModified.Format(http.TimeFormat))
+ }
+
+ resp, err := http_getter.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 304 {
+ log.Println("Weather was not modified yet (got a 304)")
+ return nil
+ } else if resp.StatusCode != 200 {
+ log.Println("Warning: Got a non-200 response from the weather API: %d", resp.StatusCode)
+ }
+
+ updateTimeFromHeaderIfOK(resp.Header, "Expires", &wp.expires)
+ updateTimeFromHeaderIfOK(resp.Header, "Last-Modified", &wp.lastModified)
+
+ var apiResponse ApiResponse
+ dec := json.NewDecoder(resp.Body)
+ if err := dec.Decode(&apiResponse); err != nil {
+ return fmt.Errorf("Failed decoding weather API response: %w", err)
+ }
+
+ wp.timeseries = apiResponse.Properties.Timeseries
+
+ return nil
+}
+
+func (wp *WeatherProvider) CurrentWeather() (*TimeseriesEntry, error) {
wp.intervalRunner.Run(func() bool {
- log.Printf("Getting new weather data")
- wp.weather, wp.err = CurrentWeather(wp.place)
+ wp.err = wp.update()
if wp.err == nil {
- log.Printf("Successfully loaded weather data")
+ log.Printf("Successfully updated weather data")
} else {
- log.Printf("Failed loading weather data: %s", wp.err)
+ log.Printf("Failed updating weather data: %s", wp.err)
}
return wp.err == nil
})
- return wp.weather, wp.err
+ if wp.err != nil {
+ return nil, wp.err
+ }
+
+ return wp.timeseries.Current(time.Now()), nil
}