use std::sync::RwLock;
use chrono::{DateTime, Utc};
use reqwest::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CONNECTION, COOKIE, HeaderMap, HeaderValue, REFERER,
USER_AGENT,
};
pub mod ts;
#[derive(Debug, serde::Deserialize)]
pub struct ForecastDay {
pub day_string: String,
#[serde(with = "chrono::serde::ts_milliseconds")]
pub min_date: DateTime<Utc>,
#[serde(with = "chrono::serde::ts_milliseconds")]
pub max_date: DateTime<Utc>,
pub new_day: Option<i64>,
pub current_time_string: Option<String>,
pub current_time: Option<i64>,
#[serde(deserialize_with = "ts::deserialize_ts")]
pub rainfall: ts::TimeSeries,
#[serde(
deserialize_with = "ts::deserialize_ts_variance",
rename = "variance_rain"
)]
pub rainfall_variance: ts::TimeSeriesVariance,
#[serde(with = "chrono::serde::ts_milliseconds")]
pub sunrise: DateTime<Utc>,
#[serde(with = "chrono::serde::ts_milliseconds")]
pub sunset: DateTime<Utc>,
#[serde(deserialize_with = "ts::deserialize_ts")]
pub sunshine: ts::TimeSeries,
#[serde(deserialize_with = "ts::deserialize_ts")]
pub temperature: ts::TimeSeries,
#[serde(
deserialize_with = "ts::deserialize_ts_variance",
rename = "variance_range"
)]
pub temperature_variance: ts::TimeSeriesVariance,
#[serde(deserialize_with = "ts::deserialize_ts_inside_data")]
pub wind: ts::TimeSeries,
#[serde(deserialize_with = "ts::deserialize_ts_variance")]
pub wind_speed_variance: ts::TimeSeriesVariance,
#[serde(deserialize_with = "ts::deserialize_ts_inside_data")]
pub wind_gust_peak: ts::TimeSeries,
#[serde(deserialize_with = "ts::deserialize_ts_variance")]
pub wind_gust_speed_variance: ts::TimeSeriesVariance,
}
#[derive(Debug, serde::Deserialize)]
pub struct Forecast {
#[serde(flatten)]
pub forecasts: Vec<ForecastDay>,
}
#[derive(serde::Deserialize)]
struct ApiVersions {
#[serde(rename = "forecast-chart")]
forecast_chart: String,
}
pub struct MeteoSuisseClient {
root_url: String,
api_versions: RwLock<Option<ApiVersions>>,
last_versions_fetch: RwLock<Option<DateTime<Utc>>>,
}
impl MeteoSuisseClient {
pub fn new(root_url: &str) -> Self {
MeteoSuisseClient {
root_url: root_url.to_string(),
api_versions: RwLock::new(None),
last_versions_fetch: RwLock::new(None),
}
}
pub fn default() -> Self {
MeteoSuisseClient::new("https://www.meteosuisse.admin.ch/product/output/")
}
async fn get_api_versions(&self) -> Result<ApiVersions, Box<dyn std::error::Error>> {
let data = self.get("versions.json").await?;
let versions: ApiVersions = serde_json::from_str(&data)?;
Ok(versions)
}
async fn refresh_api_versions(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut last_fetch = self.last_versions_fetch.write().unwrap();
if let Some(last) = *last_fetch {
if chrono::Utc::now().signed_duration_since(last).num_minutes() < 10 {
return Ok(());
}
}
let versions = self.get_api_versions().await?;
let mut api_versions = self.api_versions.write().unwrap();
*api_versions = Some(versions);
*last_fetch = Some(chrono::Utc::now());
Ok(())
}
async fn get(&self, endpoint: &str) -> Result<String, Box<dyn std::error::Error>> {
let url = format!("{}{}", self.root_url, endpoint);
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_static(
"Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0",
),
);
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("fr-CH,en;q=0.5"));
headers.insert(
ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br, zstd"),
);
headers.insert(
REFERER,
HeaderValue::from_static(
"https://www.meteosuisse.admin.ch/previsions-locales/jussy/1254.html",
),
);
headers.insert("Sec-GPC", HeaderValue::from_static("1"));
headers.insert(
"Alt-Used",
HeaderValue::from_static("www.meteosuisse.admin.ch"),
);
headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("empty"));
headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("cors"));
headers.insert("Sec-Fetch-Site", HeaderValue::from_static("same-origin"));
let client = reqwest::Client::builder()
.default_headers(headers)
.gzip(true) .deflate(true) .build()?;
let resp = client.get(url).send().await?;
let body = resp.text().await?;
Ok(body)
}
pub fn get_from_file(&self, path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
pub async fn get_forecast(
&self,
location_id: &str,
) -> Result<Vec<ForecastDay>, Box<dyn std::error::Error>> {
self.refresh_api_versions().await?;
let version_forecast = self.api_versions.read().unwrap();
let version = &version_forecast.as_ref().unwrap().forecast_chart.clone();
let data = self
.get(&format!(
"forecast-chart/version__{}/fr/{}.json",
version, location_id
))
.await?;
let s = serde_json::from_str(&data)?;
Ok(s)
}
}