Skip to content

Commit

Permalink
Use lat/lon for Open Weather Map & add zip option (#1948)
Browse files Browse the repository at this point in the history
Fixes #1485
  • Loading branch information
bim9262 authored Oct 5, 2023
1 parent 679e9f3 commit ca2e868
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 26 deletions.
19 changes: 11 additions & 8 deletions src/blocks/weather.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
//!
Expand Down Expand Up @@ -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<dyn WeatherProvider + Send + Sync> = 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);
Expand Down
109 changes: 91 additions & 18 deletions src/blocks/weather/open_weather_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -15,6 +17,8 @@ pub struct Config {
city_id: Option<String>,
#[serde(default = "getenv_openweathermap_place")]
place: Option<String>,
#[serde(default = "getenv_openweathermap_zip")]
zip: Option<String>,
coordinates: Option<(String, String)>,
#[serde(default)]
units: UnitSystem,
Expand All @@ -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<String>,
}

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<Service<'a>> {
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<Option<String>> {
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::<Vec<CityCoord>>()
.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)
}
}

Expand All @@ -41,6 +115,9 @@ fn getenv_openweathermap_city_id() -> Option<String> {
fn getenv_openweathermap_place() -> Option<String> {
std::env::var(PLACE_ENV).ok()
}
fn getenv_openweathermap_zip() -> Option<String> {
std::env::var(ZIP_ENV).ok()
}
fn default_lang() -> String {
"en".into()
}
Expand Down Expand Up @@ -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<Coordinates>) -> Result<WeatherResult> {
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
Expand All @@ -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,
},
Expand Down

0 comments on commit ca2e868

Please sign in to comment.