2022-02-13 12:45:27 +01:00
|
|
|
//! The Luchtmeetnet open data provider.
|
|
|
|
//!
|
|
|
|
//! For more information about Luchtmeetnet, see: <https://www.luchtmeetnet.nl/contact>.
|
|
|
|
|
2022-02-14 21:06:31 +01:00
|
|
|
use cached::proc_macro::cached;
|
2022-02-13 12:45:27 +01:00
|
|
|
use chrono::serde::ts_seconds;
|
|
|
|
use chrono::{DateTime, Utc};
|
2022-02-13 13:10:12 +01:00
|
|
|
use reqwest::Url;
|
2022-02-13 12:45:27 +01:00
|
|
|
use rocket::serde::{Deserialize, Serialize};
|
|
|
|
|
2022-02-15 14:15:59 +01:00
|
|
|
use crate::position::Position;
|
|
|
|
use crate::Metric;
|
2022-02-13 12:45:27 +01:00
|
|
|
|
|
|
|
/// The base URL for the Luchtmeetnet API.
|
|
|
|
const LUCHTMEETNET_BASE_URL: &str = "https://api.luchtmeetnet.nl/open_api/concentrations";
|
|
|
|
|
|
|
|
/// The Luchtmeetnet API data container.
|
|
|
|
///
|
|
|
|
/// This is only used temporarily during deserialization.
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[serde(crate = "rocket::serde")]
|
|
|
|
struct Container {
|
|
|
|
data: Vec<Item>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The Luchtmeetnet API data item.
|
2022-02-14 21:06:31 +01:00
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
2022-02-13 12:45:27 +01:00
|
|
|
#[serde(crate = "rocket::serde")]
|
|
|
|
pub(crate) struct Item {
|
2022-02-13 15:39:09 +01:00
|
|
|
/// The time(stamp) of the forecast.
|
2022-02-13 12:45:27 +01:00
|
|
|
#[serde(
|
|
|
|
rename(deserialize = "timestamp_measured"),
|
|
|
|
serialize_with = "ts_seconds::serialize"
|
|
|
|
)]
|
|
|
|
time: DateTime<Utc>,
|
2022-02-13 15:39:09 +01:00
|
|
|
|
2022-02-13 12:45:27 +01:00
|
|
|
/// The forecasted value.
|
|
|
|
///
|
|
|
|
/// The unit depends on the selected [metric](Metric).
|
|
|
|
value: f32,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Retrieves the Luchtmeetnet forecasted items for the provided position and metric.
|
|
|
|
///
|
2022-02-13 15:39:09 +01:00
|
|
|
/// It supports the following metrics:
|
|
|
|
/// * [`Metric::AQI`]
|
|
|
|
/// * [`Metric::NO2`]
|
|
|
|
/// * [`Metric::O3`]
|
|
|
|
/// * [`Metric::PM10`]
|
|
|
|
///
|
2022-02-13 12:45:27 +01:00
|
|
|
/// Returns [`None`] if retrieval or deserialization fails, or if the metric is not supported by
|
|
|
|
/// this provider.
|
2022-02-14 21:40:07 +01:00
|
|
|
///
|
|
|
|
/// If the result is [`Some`] it will be cached for 30 minutes for the the given position and
|
|
|
|
/// metric.
|
2022-02-15 14:15:59 +01:00
|
|
|
#[cached(time = 1800, option = true)]
|
|
|
|
pub(crate) async fn get(position: Position, metric: Metric) -> Option<Vec<Item>> {
|
2022-02-13 12:45:27 +01:00
|
|
|
let formula = match metric {
|
|
|
|
Metric::AQI => "lki",
|
|
|
|
Metric::NO2 => "no2",
|
|
|
|
Metric::O3 => "o3",
|
|
|
|
Metric::PM10 => "pm10",
|
|
|
|
_ => return None, // Unsupported metric
|
|
|
|
};
|
2022-02-13 13:10:12 +01:00
|
|
|
let mut url = Url::parse(LUCHTMEETNET_BASE_URL).unwrap();
|
|
|
|
url.query_pairs_mut()
|
|
|
|
.append_pair("formula", formula)
|
2022-02-15 14:15:59 +01:00
|
|
|
.append_pair("latitude", &position.lat_as_str(5))
|
|
|
|
.append_pair("longitude", &position.lon_as_str(5));
|
2022-02-13 12:45:27 +01:00
|
|
|
|
2022-02-14 21:13:35 +01:00
|
|
|
println!("▶️ Retrieving Luchtmeetnet data from: {url}");
|
2022-02-13 13:10:12 +01:00
|
|
|
let response = reqwest::get(url).await.ok()?;
|
2022-02-13 12:45:27 +01:00
|
|
|
let root: Container = match response.error_for_status() {
|
|
|
|
Ok(res) => res.json().await.ok()?,
|
|
|
|
Err(_err) => return None,
|
|
|
|
};
|
|
|
|
|
|
|
|
Some(root.data)
|
|
|
|
}
|