summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.markdown5
-rw-r--r--config.go4
-rw-r--r--http_getter/http_getter.go19
-rw-r--r--main.go25
-rw-r--r--templates/template.html4
-rw-r--r--weather/weather.go193
6 files changed, 189 insertions, 61 deletions
diff --git a/README.markdown b/README.markdown
index aa87dbb..0a4037c 100644
--- a/README.markdown
+++ b/README.markdown
@@ -15,7 +15,10 @@ Here is an example with all fields filled out.
{
// The place for which to get the weather data. If omitted, no weather will be shown
- "WeatherPlace": "Germany/Hamburg/Hamburg",
+ "WeatherCoords": {
+ "Lat": "53.6",
+ "Lon": "10.0"
+ },
// A list of links to show. Can be omitted.
"Links": [
diff --git a/config.go b/config.go
index 9464291..ec4f8ec 100644
--- a/config.go
+++ b/config.go
@@ -10,7 +10,9 @@ import (
// Config contains all configuration options that are read from .config/startpage/config.json
type Config struct {
// The place for which to get the weather data. If omitted, no weather will be shown
- WeatherPlace string
+ WeatherCoords struct {
+ Lat, Lon string
+ }
// A list of links to show
Links []Link
diff --git a/http_getter/http_getter.go b/http_getter/http_getter.go
index 5df3ac2..9fdf7a5 100644
--- a/http_getter/http_getter.go
+++ b/http_getter/http_getter.go
@@ -4,16 +4,27 @@ import (
"net/http"
)
-// Get is like http.Get, but we're sending our own user agent.
-func Get(url string) (*http.Response, error) {
- client := &http.Client{}
-
+func BuildGetRequest(url string) (*http.Request, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", "github.com/slivasur/startpage")
+ return req, nil
+}
+func Do(req *http.Request) (*http.Response, error) {
+ client := &http.Client{}
return client.Do(req)
}
+
+// Get is like http.Get, but we're sending our own user agent.
+func Get(url string) (*http.Response, error) {
+ req, err := BuildGetRequest(url)
+ if err != nil {
+ return nil, err
+ }
+
+ return Do(req)
+}
diff --git a/main.go b/main.go
index 01c8c60..776b1a1 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"log"
+ "math"
"net/http"
"os"
"path"
@@ -52,11 +53,11 @@ func loadTemplate(templateDir string) error {
}
func buildWeatherProvider(config Config) *weather.WeatherProvider {
- if config.WeatherPlace == "" {
+ if config.WeatherCoords.Lat == "" || config.WeatherCoords.Lon == "" {
return nil
}
- return weather.NewWeatherProvider(config.WeatherPlace)
+ return weather.NewWeatherProvider(config.WeatherCoords.Lat, config.WeatherCoords.Lon)
}
func buildRedditImageProvider(config Config) *reddit_background.RedditImageProvider {
@@ -96,9 +97,23 @@ func main() {
log.Fatal(http.ListenAndServe(*laddr, nil))
}
+type TplWeather struct {
+ Temp int
+}
+
+func convertWeather(ts *weather.TimeseriesEntry) *TplWeather {
+ if ts == nil {
+ return nil
+ }
+
+ return &TplWeather{
+ Temp: int(math.Round(ts.Temperature())),
+ }
+}
+
type TplData struct {
BgImage *reddit_background.RedditImageForAjax
- Weather *weather.Weather
+ Weather *TplWeather
Links []Link
CanSaveBg bool
}
@@ -109,7 +124,7 @@ func startpage(config Config, redditImageProvider *reddit_background.RedditImage
return func(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
- var curWeather *weather.Weather = nil
+ var curWeather *weather.TimeseriesEntry = nil
if weatherProvider != nil {
var err error
if curWeather, err = weatherProvider.CurrentWeather(); err != nil {
@@ -119,7 +134,7 @@ func startpage(config Config, redditImageProvider *reddit_background.RedditImage
if err := tpl.Execute(rw, &TplData{
redditImageProvider.Image().ForAjax(),
- curWeather,
+ convertWeather(curWeather),
config.Links,
config.BackgroundSavepath != "",
}); err != nil {
diff --git a/templates/template.html b/templates/template.html
index 4b20536..7592b50 100644
--- a/templates/template.html
+++ b/templates/template.html
@@ -80,8 +80,7 @@
<body {{ if .BgImage }} style="background-color: black; background-image: url(/bgimg);"{{ end }}>
{{ if .Weather }}
<div id="weather">
- <a href="{{ .Weather.URL }}"><img src="{{ .Weather.Icon }}" alt="" /></a>
- <span id="temp">{{ .Weather.Temp.Value }}°</span>
+ <span id="temp">{{ .Weather.Temp }}°</span>
</div>
{{ end }}
{{ if .Links }}
@@ -94,7 +93,6 @@
</div>
{{ end }}
-
<footer>
{{ if .Weather }}
<div id="yr_no_credit">Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK</div>
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
}