forked from Elara6331/itd
248 lines
5.8 KiB
Go
248 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.elara.ws/itd/infinitime"
|
|
"go.elara.ws/logger/log"
|
|
)
|
|
|
|
// METResponse represents a response from
|
|
// the MET Norway API
|
|
type METResponse struct {
|
|
Properties struct {
|
|
Timeseries []struct {
|
|
Time time.Time
|
|
Data METData
|
|
}
|
|
}
|
|
}
|
|
|
|
// METData represents data in a METResponse
|
|
type METData struct {
|
|
Instant struct {
|
|
Details struct {
|
|
AirPressure float32 `json:"air_pressure_at_sea_level"`
|
|
Temperature float32 `json:"air_temperature"`
|
|
DewPoint float32 `json:"dew_point_temperature"`
|
|
CloudAreaFraction float32 `json:"cloud_area_fraction"`
|
|
FogAreaFraction float32 `json:"fog_area_fraction"`
|
|
RelativeHumidity float32 `json:"relative_humidity"`
|
|
UVIndex float32 `json:"ultraviolet_index_clear_sky"`
|
|
WindDirection float32 `json:"wind_from_direction"`
|
|
WindSpeed float32 `json:"wind_speed"`
|
|
}
|
|
}
|
|
NextHour struct {
|
|
Summary struct {
|
|
SymbolCode string `json:"symbol_code"`
|
|
}
|
|
Details struct {
|
|
PrecipitationAmount float32 `json:"precipitation_amount"`
|
|
}
|
|
} `json:"next_1_hours"`
|
|
Next6Hours struct {
|
|
Details struct {
|
|
MaxTemp float32 `json:"air_temperature_max"`
|
|
MinTemp float32 `json:"air_temperature_min"`
|
|
}
|
|
} `json:"next_6_hours"`
|
|
}
|
|
|
|
// OSMData represents lat/long data from
|
|
// OpenStreetMap Nominatim
|
|
type OSMData []struct {
|
|
Lat string `json:"lat"`
|
|
Lon string `json:"lon"`
|
|
}
|
|
|
|
var sendWeatherCh = make(chan struct{}, 1)
|
|
|
|
func sleepCtx(ctx context.Context, d time.Duration) {
|
|
select {
|
|
case <-time.After(d):
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
|
if !k.Bool("weather.enabled") {
|
|
return nil
|
|
}
|
|
|
|
// Get location based on string in config
|
|
lat, lon, err := getLocation(ctx, k.String("weather.location"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
timer := time.NewTimer(time.Hour)
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done("weather")
|
|
for {
|
|
select {
|
|
case _, ok := <-ctx.Done():
|
|
if !ok {
|
|
return
|
|
}
|
|
default:
|
|
}
|
|
|
|
// Attempt to get weather
|
|
data, err := getWeather(ctx, lat, lon)
|
|
if err != nil {
|
|
log.Warn("Error getting weather data").Err(err).Send()
|
|
// Wait 15 minutes before retrying
|
|
sleepCtx(ctx, 15*time.Minute)
|
|
continue
|
|
}
|
|
|
|
// Get current data
|
|
current := data.Properties.Timeseries[0]
|
|
currentData := current.Data.Instant.Details
|
|
|
|
icon := parseSymbol(current.Data.NextHour.Summary.SymbolCode)
|
|
if icon == infinitime.WeatherIconClear {
|
|
switch {
|
|
case currentData.CloudAreaFraction > 50:
|
|
icon = infinitime.WeatherIconHeavyClouds
|
|
case currentData.CloudAreaFraction == 50:
|
|
icon = infinitime.WeatherIconClouds
|
|
case currentData.CloudAreaFraction > 0:
|
|
icon = infinitime.WeatherIconFewClouds
|
|
}
|
|
}
|
|
|
|
err = dev.SetCurrentWeather(infinitime.CurrentWeather{
|
|
Time: time.Now(),
|
|
CurrentTemp: currentData.Temperature,
|
|
MaxTemp: current.Data.Next6Hours.Details.MaxTemp,
|
|
MinTemp: current.Data.Next6Hours.Details.MinTemp,
|
|
Location: k.String("weather.location"),
|
|
Icon: icon,
|
|
})
|
|
if err != nil {
|
|
log.Error("Error setting weather").Err(err).Send()
|
|
}
|
|
|
|
// Reset timer to 1 hour
|
|
timer.Stop()
|
|
timer.Reset(time.Hour)
|
|
|
|
// Wait for timer to fire or manual update signal
|
|
select {
|
|
case <-timer.C:
|
|
case <-sendWeatherCh:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// getLocation returns the latitude and longitude
|
|
// given a location
|
|
func getLocation(ctx context.Context, loc string) (lat, lon float64, err error) {
|
|
// Create request URL and perform GET request
|
|
reqURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search.php?q=%s&format=jsonv2", url.QueryEscape(loc))
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.elara.ws/Elara6331/itd", strings.TrimSpace(version)))
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Decode JSON from response into OSMData
|
|
data := OSMData{}
|
|
err = json.NewDecoder(res.Body).Decode(&data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// If no data points
|
|
if len(data) == 0 {
|
|
return
|
|
}
|
|
|
|
// Get first data point
|
|
out := data[0]
|
|
|
|
// Attempt to parse latitude
|
|
lat, err = strconv.ParseFloat(out.Lat, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Attempt to parse longitude
|
|
lon, err = strconv.ParseFloat(out.Lon, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// getWeather gets weather data given a latitude and longitude
|
|
func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
|
|
// Create new GET request
|
|
req, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodGet,
|
|
fmt.Sprintf(
|
|
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f",
|
|
lat,
|
|
lon,
|
|
),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set identifying user agent as per NMI requirements
|
|
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.elara.ws/Elara6331/itd", strings.TrimSpace(version)))
|
|
|
|
// Perform request
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Decode JSON from response to METResponse struct
|
|
out := &METResponse{}
|
|
err = json.NewDecoder(res.Body).Decode(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// parseSymbol determines what weather icon a symbol code codes for.
|
|
func parseSymbol(symCode string) infinitime.WeatherIcon {
|
|
switch {
|
|
case strings.Contains(symCode, "lightrain"):
|
|
return infinitime.WeatherIconRain
|
|
case strings.Contains(symCode, "rain"):
|
|
return infinitime.WeatherIconCloudsWithRain
|
|
case strings.Contains(symCode, "snow"),
|
|
strings.Contains(symCode, "sleet"):
|
|
return infinitime.WeatherIconSnow
|
|
case strings.Contains(symCode, "thunder"):
|
|
return infinitime.WeatherIconThunderstorm
|
|
default:
|
|
return infinitime.WeatherIconClear
|
|
}
|
|
}
|