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"
// Units openweathermap 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 string = "owm_response"
// CacheKeyURL key used when caching the url responsible for the response
@ -48,6 +52,11 @@ type owmDataResponse struct {
temperature `json:"main"`
}
type geoLocation struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
func (d *Owm) Enabled() bool {
err := d.setStatus()
return err == nil
@ -76,9 +85,28 @@ func (d *Owm) getResult() (*owmDataResponse, error) {
apikey := d.props.GetString(APIKey, ".")
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")
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)
if err != nil {

View file

@ -12,56 +12,107 @@ import (
)
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) {
cases := []struct {
Case string
JSONResponse string
ExpectedString string
ExpectedEnabled bool
Template string
Error error
Case string
Location string
Latitude float64
Longitude float64
GeoJSONResponse string
WeatherJSONResponse string
ExpectedString string
ExpectedEnabled bool
Template string
Error error
}{
{
Case: "Sunny Display",
JSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
Case: "Sunny Display",
Location: "AMSTERDAM,NL",
GeoJSONResponse: `[{"lat": 52.3727598,"lon": 4.8936041}]`,
WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
},
{
Case: "Sunny Display",
JSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
Template: "{{.Weather}} ({{.Temperature}}{{.UnitIcon}})",
Case: "Sunny Display",
Location: "AMSTERDAM,NL",
GeoJSONResponse: `[{"lat": 52.3727598,"lon": 4.8936041}]`,
WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d (20°C)",
ExpectedEnabled: true,
Template: "{{.Weather}} ({{.Temperature}}{{.UnitIcon}})",
},
{
Case: "Sunny Display",
JSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d",
ExpectedEnabled: true,
Template: "{{.Weather}} ",
Case: "Sunny Display",
Location: "AMSTERDAM,NL",
GeoJSONResponse: `[{"lat": 52.3727598,"lon": 4.8936041}]`,
WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`,
ExpectedString: "\ue30d",
ExpectedEnabled: true,
Template: "{{.Weather}} ",
},
{
Case: "Error in retrieving data",
JSONResponse: "nonsense",
Error: errors.New("Something went wrong"),
ExpectedEnabled: false,
Case: "Config Skip Geocoding Check With Location",
Location: "AMSTERDAM,NL",
Latitude: 52.3727598,
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 {
env := &mock.MockedEnvironment{}
props := properties.Map{
APIKey: "key",
Location: "AMSTERDAM,NL",
Units: "metric",
properties.CacheTimeout: 0,
var props properties.Map
if tc.Latitude != 0 && tc.Longitude != 0 && tc.Location != "" {
props = properties.Map{
APIKey: "key",
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{
props: props,
@ -179,14 +230,16 @@ func TestOWMSegmentIcons(t *testing.T) {
ExpectedIconString: "\ue313",
},
}
geoResponse := `[{"lat": 52.3727598,"lon": 4.8936041}]`
for _, tc := range cases {
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)
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{
props: properties.Map{
@ -206,10 +259,11 @@ func TestOWMSegmentIcons(t *testing.T) {
for _, tc := range cases {
env := &mock.MockedEnvironment{}
response := 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)
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?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{
props: properties.Map{
@ -240,7 +294,7 @@ func TestOWMSegmentFromCache(t *testing.T) {
env: env,
}
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()
env.On("Cache").Return(cache)
@ -250,7 +304,7 @@ func TestOWMSegmentFromCache(t *testing.T) {
func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
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{}
cache := &mock.MockedCache{}
@ -264,7 +318,7 @@ func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
env: env,
}
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()
env.On("Cache").Return(cache)

View file

@ -35,14 +35,20 @@ import Config from '@site/src/components/Config.js';
## Properties
| Name | Type | Description |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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` |
| `units` | `string` | Units of measurement. Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit) - defaults to `standard` |
| `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}})` |
| Name | Type | Description |
| --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `apikey` | `string` | Your API key from [Open Weather Map][owm] |
| `latitude` | `float64` | The latitude of the requested location. |
| `longitude` | `float64` | The longitude of the requested location. |
| `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` |
| `units` | `string` | Units of measurement. Available values are standard (kelvin), metric (celsius), and imperial (fahrenheit) - defaults to `standard` |
| `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])