feat: update Open Weather Map to use Geocoding API

Updated the Open Weather Map Current Weather URL to remove the
deprecated query parameter. Updated the URL to add the supported
latitude and longitude parameters.

Added a call to the Open Wetaher Map Geocoding API to resolve the
location parameter to latitude and longitude values as specified in the
Open Weather Map documentation.

Added properties to the Open Weather Map segment to allow users to
manually specify the latitude and longitude if desired. Doing this will
skip the geocoding API call and ignore the location parameter.

Updated the website documentation for the Open Weather Map segment to
reflect the property changes and explain how to use them.
This commit is contained in:
Matthew Miller 2023-05-20 01:01:29 -04:00 committed by Jan De Dobbeleer
parent 1ca3a8d256
commit ed8d89a7cc
3 changed files with 136 additions and 48 deletions

View file

@ -28,6 +28,10 @@ const (
Location properties.Property = "location" Location properties.Property = "location"
// Units openweathermap units // Units openweathermap units
Units properties.Property = "units" Units properties.Property = "units"
// Latitude for the location used in place of location
Latitude properties.Property = "latitude"
// Longitude for the location used in place of location
Longitude properties.Property = "longitude"
// CacheKeyResponse key used when caching the response // CacheKeyResponse key used when caching the response
CacheKeyResponse string = "owm_response" CacheKeyResponse string = "owm_response"
// CacheKeyURL key used when caching the url responsible for the response // CacheKeyURL key used when caching the url responsible for the response
@ -48,6 +52,11 @@ type owmDataResponse struct {
temperature `json:"main"` temperature `json:"main"`
} }
type geoLocation struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
func (d *Owm) Enabled() bool { func (d *Owm) Enabled() bool {
err := d.setStatus() err := d.setStatus()
return err == nil return err == nil
@ -76,9 +85,28 @@ func (d *Owm) getResult() (*owmDataResponse, error) {
apikey := d.props.GetString(APIKey, ".") apikey := d.props.GetString(APIKey, ".")
location := d.props.GetString(Location, "De Bilt,NL") location := d.props.GetString(Location, "De Bilt,NL")
latitude := d.props.GetFloat64(Latitude, 91)
longitude := d.props.GetFloat64(Longitude, 181)
units := d.props.GetString(Units, "standard") units := d.props.GetString(Units, "standard")
httpTimeout := d.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout) httpTimeout := d.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
d.URL = fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&appid=%s", location, units, apikey)
if latitude > 90 || latitude < -90 || longitude > 180 && longitude < -180 {
var geoResponse []geoLocation
geocodingURL := fmt.Sprintf("http://api.openweathermap.org/geo/1.0/direct?q=%s&limit=1&appid=%s", location, apikey)
body, err := d.env.HTTPRequest(geocodingURL, nil, httpTimeout)
if err != nil {
return new(owmDataResponse), err
}
err = json.Unmarshal(body, &geoResponse)
if err != nil {
return new(owmDataResponse), err
}
latitude = geoResponse[0].Lat
longitude = geoResponse[0].Lon
}
d.URL = fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?lat=%v&lon=%v&units=%s&appid=%s", latitude, longitude, units, apikey)
body, err := d.env.HTTPRequest(d.URL, nil, httpTimeout) body, err := d.env.HTTPRequest(d.URL, nil, httpTimeout)
if err != nil { if err != nil {

View file

@ -12,56 +12,107 @@ import (
) )
const ( const (
OWMAPIURL = "http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key" OWMGEOAPIURL = "http://api.openweathermap.org/geo/1.0/direct?q=AMSTERDAM,NL&limit=1&appid=key"
OWMWEATHERAPIURL = "http://api.openweathermap.org/data/2.5/weather?lat=52.3727598&lon=4.8936041&units=metric&appid=key"
) )
func TestOWMSegmentSingle(t *testing.T) { func TestOWMSegmentSingle(t *testing.T) {
cases := []struct { cases := []struct {
Case string Case string
JSONResponse string Location string
ExpectedString string Latitude float64
ExpectedEnabled bool Longitude float64
Template string GeoJSONResponse string
Error error WeatherJSONResponse string
ExpectedString string
ExpectedEnabled bool
Template string
Error error
}{ }{
{ {
Case: "Sunny Display", Case: "Sunny Display",
JSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, Location: "AMSTERDAM,NL",
ExpectedString: "\ue30d (20°C)", GeoJSONResponse: `[{"lat": 52.3727598,"lon": 4.8936041}]`,
ExpectedEnabled: true, WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
}, },
{ {
Case: "Sunny Display", Case: "Sunny Display",
JSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, Location: "AMSTERDAM,NL",
ExpectedString: "\ue30d (20°C)", GeoJSONResponse: `[{"lat": 52.3727598,"lon": 4.8936041}]`,
ExpectedEnabled: true, WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
Template: "{{.Weather}} ({{.Temperature}}{{.UnitIcon}})", ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
Template: "{{.Weather}} ({{.Temperature}}{{.UnitIcon}})",
}, },
{ {
Case: "Sunny Display", Case: "Sunny Display",
JSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, Location: "AMSTERDAM,NL",
ExpectedString: "\ue30d", GeoJSONResponse: `[{"lat": 52.3727598,"lon": 4.8936041}]`,
ExpectedEnabled: true, WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
Template: "{{.Weather}} ", ExpectedString: "\ue30d",
ExpectedEnabled: true,
Template: "{{.Weather}} ",
}, },
{ {
Case: "Error in retrieving data", Case: "Config Skip Geocoding Check With Location",
JSONResponse: "nonsense", Location: "AMSTERDAM,NL",
Error: errors.New("Something went wrong"), Latitude: 52.3727598,
ExpectedEnabled: false, Longitude: 4.8936041,
WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
},
{
Case: "Config Skip Geocoding Check Without Location",
Latitude: 52.3727598,
Longitude: 4.8936041,
WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
},
{
Case: "Error in retrieving data",
Location: "AMSTERDAM,NL",
GeoJSONResponse: "nonsense",
WeatherJSONResponse: "nonsense",
Error: errors.New("Something went wrong"),
ExpectedEnabled: false,
}, },
} }
for _, tc := range cases { for _, tc := range cases {
env := &mock.MockedEnvironment{} env := &mock.MockedEnvironment{}
props := properties.Map{ var props properties.Map
APIKey: "key", if tc.Latitude != 0 && tc.Longitude != 0 && tc.Location != "" {
Location: "AMSTERDAM,NL", props = properties.Map{
Units: "metric", APIKey: "key",
properties.CacheTimeout: 0, Location: tc.Location,
Latitude: tc.Latitude,
Longitude: tc.Longitude,
Units: "metric",
properties.CacheTimeout: 0,
}
} else if tc.Location != "" {
props = properties.Map{
APIKey: "key",
Location: tc.Location,
Units: "metric",
properties.CacheTimeout: 0,
}
} else if tc.Latitude != 0 && tc.Longitude != 0 {
props = properties.Map{
APIKey: "key",
Latitude: tc.Latitude,
Longitude: tc.Longitude,
Units: "metric",
properties.CacheTimeout: 0,
}
} }
env.On("HTTPRequest", OWMAPIURL).Return([]byte(tc.JSONResponse), tc.Error) env.On("HTTPRequest", OWMGEOAPIURL).Return([]byte(tc.GeoJSONResponse), tc.Error)
env.On("HTTPRequest", OWMWEATHERAPIURL).Return([]byte(tc.WeatherJSONResponse), tc.Error)
o := &Owm{ o := &Owm{
props: props, props: props,
@ -179,14 +230,16 @@ func TestOWMSegmentIcons(t *testing.T) {
ExpectedIconString: "\ue313", ExpectedIconString: "\ue313",
}, },
} }
geoResponse := `[{"lat": 52.3727598,"lon": 4.8936041}]`
for _, tc := range cases { for _, tc := range cases {
env := &mock.MockedEnvironment{} env := &mock.MockedEnvironment{}
response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20.3}}`, tc.IconID) weatherResponse := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20.3}}`, tc.IconID)
expectedString := fmt.Sprintf("%s (20°C)", tc.ExpectedIconString) expectedString := fmt.Sprintf("%s (20°C)", tc.ExpectedIconString)
env.On("HTTPRequest", OWMAPIURL).Return([]byte(response), nil) env.On("HTTPRequest", OWMGEOAPIURL).Return([]byte(geoResponse), nil)
env.On("HTTPRequest", OWMWEATHERAPIURL).Return([]byte(weatherResponse), nil)
o := &Owm{ o := &Owm{
props: properties.Map{ props: properties.Map{
@ -206,10 +259,11 @@ func TestOWMSegmentIcons(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
env := &mock.MockedEnvironment{} env := &mock.MockedEnvironment{}
response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20.3}}`, tc.IconID) weatherResponse := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20.3}}`, tc.IconID)
expectedString := fmt.Sprintf("«%s (20°C)»(http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", tc.ExpectedIconString) expectedString := fmt.Sprintf("«%s (20°C)»(http://api.openweathermap.org/data/2.5/weather?lat=52.3727598&lon=4.8936041&units=metric&appid=key)", tc.ExpectedIconString)
env.On("HTTPRequest", OWMAPIURL).Return([]byte(response), nil) env.On("HTTPRequest", OWMGEOAPIURL).Return([]byte(geoResponse), nil)
env.On("HTTPRequest", OWMWEATHERAPIURL).Return([]byte(weatherResponse), nil)
o := &Owm{ o := &Owm{
props: properties.Map{ props: properties.Map{
@ -240,7 +294,7 @@ func TestOWMSegmentFromCache(t *testing.T) {
env: env, env: env,
} }
cache.On("Get", "owm_response").Return(response, true) cache.On("Get", "owm_response").Return(response, true)
cache.On("Get", "owm_url").Return("http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key", true) cache.On("Get", "owm_url").Return("http://api.openweathermap.org/data/2.5/weather?lat=52.3727598&lon=4.8936041&units=metric&appid=key", true)
cache.On("Set").Return() cache.On("Set").Return()
env.On("Cache").Return(cache) env.On("Cache").Return(cache)
@ -250,7 +304,7 @@ func TestOWMSegmentFromCache(t *testing.T) {
func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) { func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20}}`, "01d") response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20}}`, "01d")
expectedString := fmt.Sprintf("«%s (20°C)»(http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", "\ue30d") expectedString := fmt.Sprintf("«%s (20°C)»(http://api.openweathermap.org/data/2.5/weather?lat=52.3727598&lon=4.8936041&units=metric&appid=key)", "\ue30d")
env := &mock.MockedEnvironment{} env := &mock.MockedEnvironment{}
cache := &mock.MockedCache{} cache := &mock.MockedCache{}
@ -264,7 +318,7 @@ func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
env: env, env: env,
} }
cache.On("Get", "owm_response").Return(response, true) cache.On("Get", "owm_response").Return(response, true)
cache.On("Get", "owm_url").Return("http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key", true) cache.On("Get", "owm_url").Return("http://api.openweathermap.org/data/2.5/weather?lat=52.3727598&lon=4.8936041&units=metric&appid=key", true)
cache.On("Set").Return() cache.On("Set").Return()
env.On("Cache").Return(cache) env.On("Cache").Return(cache)

View file

@ -35,14 +35,20 @@ import Config from '@site/src/components/Config.js';
## Properties ## Properties
| Name | Type | Description | | Name | Type | Description |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `apikey` | `string` | Your API key from [Open Weather Map][owm] | | `apikey` | `string` | Your API key from [Open Weather Map][owm] |
| `location` | `string` | The requested location. Formatted as <City,STATE,COUNTRY_CODE>. City name, state code and country code divided by comma. Please, refer to ISO 3166 for the state codes or country codes - defaults to `DE BILT,NL` | | `latitude` | `float64` | The latitude of the requested location. |
| `units` | `string` | Units of measurement. Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit) - defaults to `standard` | | `longitude` | `float64` | The longitude of the requested location. |
| `http_timeout` | `int` | The default timeout for http request is 20ms. | | `location` | `string` | The requested location. Formatted as <City,STATE,COUNTRY_CODE>. City name, state code and country code divided by comma. Please, refer to ISO 3166 for the state codes or country codes - defaults to `DE BILT,NL` |
| `cache_timeout` | `int` | The default timeout for request caching is 10m. A value of 0 disables the cache. | | `units` | `string` | Units of measurement. Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit) - defaults to `standard` |
| `template` | `string` | A go [text/template][go-text-template] template extended with [sprig][sprig] utilizing the properties below - defaults to `{{.Weather}} ({{.Temperature}}{{.UnitIcon}})` | | `http_timeout` | `int` | The default timeout for http request is 20ms. |
| `cache_timeout` | `int` | The default timeout for request caching is 10m. A value of 0 disables the cache. |
| `template` | `string` | A go [text/template][go-text-template] template extended with [sprig][sprig] utilizing the properties below - defaults to `{{.Weather}} ({{.Temperature}}{{.UnitIcon}})` |
### Specifying Location
The given location can either be specified through the Latitude and Longitude properties or through the Location property. If both Latitude and Longitude are specified and valid then Location will be ignored if it is included. If Latitude or Longitude are not specified or are invalid then Location will be used.
## Template ([info][templates]) ## Template ([info][templates])