diff --git a/src/blocks/weather.rs b/src/blocks/weather.rs index e0d5e6311b..83c14d06ed 100644 --- a/src/blocks/weather.rs +++ b/src/blocks/weather.rs @@ -16,7 +16,7 @@ //! `service` | The configuration of a weather service (see below). | **Required** //! `format` | A string to customise the output of this block. See below for available placeholders. Text may need to be escaped, refer to [Escaping Text](#escaping-text). | `" $icon $weather $temp "` //! `interval` | Update interval, in seconds. | `600` -//! `autolocate` | Gets your location using the ipapi.co IP location service (no API key required). If the API call fails then the block will fallback to `city_id` or `place`. | `false` +//! `autolocate` | Gets your location using the ipapi.co IP location service (no API key required). If the API call fails then the block will fallback to service specific location config. | `false` //! `autolocate_interval` | Update interval for `autolocate` in seconds or "once" | `interval` //! //! # OpenWeatherMap Options @@ -27,17 +27,18 @@ //! ----|--------|----------|-------- //! `name` | `openweathermap`. | Yes | None //! `api_key` | Your OpenWeatherMap API key. | Yes | None -//! `city_id` | OpenWeatherMap's ID for the city. | Yes* | None -//! `place` | OpenWeatherMap 'By city name' search query. See [here](https://openweathermap.org/current) | Yes* | None //! `coordinates` | GPS latitude longitude coordinates as a tuple, example: `["39.2362","9.3317"]` | Yes* | None +//! `city_id` | OpenWeatherMap's ID for the city. (Deprecated) | Yes* | None +//! `place` | OpenWeatherMap 'By {city name},{state code},{country code}' search query. See [here](https://openweathermap.org/api/geocoding-api#direct_name). Consumes an additional API call | Yes* | None +//! `zip` | OpenWeatherMap 'By {zip code},{country code}' search query. See [here](https://openweathermap.org/api/geocoding-api#direct_zip). Consumes an additional API call | Yes* | None //! `units` | Either `"metric"` or `"imperial"`. | No | `"metric"` //! `lang` | Language code. See [here](https://openweathermap.org/current#multi). Currently only affects `weather_verbose` key. | No | `"en"` //! -//! One of `city_id`, `place` or `coordinates` is required. If more than one are supplied, `city_id` takes precedence over `place` which takes place over `coordinates`. +//! One of `coordinates`, `city_id`, `place`, or `zip` is required. If more than one are supplied, `coordinates` takes precedence over `city_id` which takes precedence over `place` which takes precedence over `zip`. //! -//! The options `api_key`, `city_id`, `place` can be omitted from configuration, +//! The options `api_key`, `city_id`, `place`, `zip`, can be omitted from configuration, //! in which case they must be provided in the environment variables -//! `OPENWEATHERMAP_API_KEY`, `OPENWEATHERMAP_CITY_ID`, `OPENWEATHERMAP_PLACE`. +//! `OPENWEATHERMAP_API_KEY`, `OPENWEATHERMAP_CITY_ID`, `OPENWEATHERMAP_PLACE`, `OPENWEATHERMAP_ZIP`. //! //! # met.no Options //! @@ -201,8 +202,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let format = config.format.with_default(" $icon $weather $temp ")?; let provider: Box = match &config.service { - WeatherService::MetNo(config) => Box::new(met_no::Service::new(config)?), - WeatherService::OpenWeatherMap(config) => Box::new(open_weather_map::Service::new(config)), + WeatherService::MetNo(service_config) => Box::new(met_no::Service::new(service_config)?), + WeatherService::OpenWeatherMap(service_config) => { + Box::new(open_weather_map::Service::new(config.autolocate, service_config).await?) + } }; let autolocate_interval = config.autolocate_interval.unwrap_or(config.interval); diff --git a/src/blocks/weather/open_weather_map.rs b/src/blocks/weather/open_weather_map.rs index 6293ba1031..9ebfa4c90f 100644 --- a/src/blocks/weather/open_weather_map.rs +++ b/src/blocks/weather/open_weather_map.rs @@ -2,9 +2,11 @@ use super::*; use chrono::Utc; pub(super) const URL: &str = "https://api.openweathermap.org/data/2.5/weather"; +pub(super) const GEO_URL: &str = "https://api.openweathermap.org/geo/1.0"; pub(super) const API_KEY_ENV: &str = "OPENWEATHERMAP_API_KEY"; pub(super) const CITY_ID_ENV: &str = "OPENWEATHERMAP_CITY_ID"; pub(super) const PLACE_ENV: &str = "OPENWEATHERMAP_PLACE"; +pub(super) const ZIP_ENV: &str = "OPENWEATHERMAP_ZIP"; #[derive(Deserialize, Debug)] #[serde(tag = "name", rename_all = "lowercase")] @@ -15,6 +17,8 @@ pub struct Config { city_id: Option, #[serde(default = "getenv_openweathermap_place")] place: Option, + #[serde(default = "getenv_openweathermap_zip")] + zip: Option, coordinates: Option<(String, String)>, #[serde(default)] units: UnitSystem, @@ -23,12 +27,82 @@ pub struct Config { } pub(super) struct Service<'a> { - config: &'a Config, + api_key: &'a String, + units: &'a UnitSystem, + lang: &'a String, + location_query: Option, } impl<'a> Service<'a> { - pub(super) fn new(config: &'a Config) -> Self { - Self { config } + pub(super) async fn new(autolocate: bool, config: &'a Config) -> Result> { + let api_key = config.api_key.as_ref().or_error(|| { + format!("missing key 'service.api_key' and environment variable {API_KEY_ENV}",) + })?; + Ok(Self { + api_key, + units: &config.units, + lang: &config.lang, + location_query: Service::get_location_query(autolocate, api_key, config).await?, + }) + } + + async fn get_location_query( + autolocate: bool, + api_key: &String, + config: &Config, + ) -> Result> { + if autolocate { + return Ok(None); + } + + let mut location_query = config + .coordinates + .as_ref() + .map(|(lat, lon)| format!("lat={lat}&lon={lon}")) + .or_else(|| config.city_id.as_ref().map(|x| format!("id={x}"))); + + location_query = match location_query { + Some(x) => Some(x), + None => match config.place.as_ref() { + Some(place) => { + let url = format!("{GEO_URL}/direct?q={place}&appid={api_key}"); + + REQWEST_CLIENT + .get(url) + .send() + .await + .error("Geo request failed")? + .json::>() + .await + .error("Geo failed to parse json")? + .first() + .map(|city| format!("lat={}&lon={}", city.lat, city.lon)) + } + None => None, + }, + }; + + location_query = match location_query { + Some(x) => Some(x), + None => match config.zip.as_ref() { + Some(zip) => { + let url = format!("{GEO_URL}/zip?zip={zip}&appid={api_key}"); + let city: CityCoord = REQWEST_CLIENT + .get(url) + .send() + .await + .error("Geo request failed")? + .json() + .await + .error("Geo failed to parse json")?; + + Some(format!("lat={}&lon={}", city.lat, city.lon)) + } + None => None, + }, + }; + + Ok(location_query) } } @@ -41,6 +115,9 @@ fn getenv_openweathermap_city_id() -> Option { fn getenv_openweathermap_place() -> Option { std::env::var(PLACE_ENV).ok() } +fn getenv_openweathermap_zip() -> Option { + std::env::var(ZIP_ENV).ok() +} fn default_lang() -> String { "en".into() } @@ -79,33 +156,29 @@ struct ApiWeather { description: String, } +#[derive(Deserialize, Debug)] +struct CityCoord { + lat: f64, + lon: f64, +} + #[async_trait] impl WeatherProvider for Service<'_> { async fn get_weather(&self, autolocated: Option) -> Result { - let api_key = self.config.api_key.as_ref().or_error(|| { - format!("missing key 'service.api_key' and environment variable {API_KEY_ENV}",) - })?; - let location_query = autolocated .map(|al| format!("lat={}&lon={}", al.latitude, al.longitude)) - .or_else(|| { - self.config - .coordinates - .as_ref() - .map(|(lat, lon)| format!("lat={lat}&lon={lon}")) - }) - .or_else(|| self.config.city_id.as_ref().map(|x| format!("id={x}"))) - .or_else(|| self.config.place.as_ref().map(|x| format!("q={x}"))) + .or_else(|| self.location_query.clone()) .error("no location was provided")?; // Refer to https://openweathermap.org/current let url = format!( "{URL}?{location_query}&appid={api_key}&units={units}&lang={lang}", - units = match self.config.units { + api_key = self.api_key, + units = match self.units { UnitSystem::Metric => "metric", UnitSystem::Imperial => "imperial", }, - lang = self.config.lang, + lang = self.lang, ); let data: ApiResponse = REQWEST_CLIENT @@ -129,7 +202,7 @@ impl WeatherProvider for Service<'_> { weather_verbose: data.weather[0].description.clone(), wind: data.wind.speed, wind_kmh: data.wind.speed - * match self.config.units { + * match self.units { UnitSystem::Metric => 3.6, UnitSystem::Imperial => 3.6 * 0.447, },