Skip to content

Commit

Permalink
block[weather]: add sunrise/sunset
Browse files Browse the repository at this point in the history
  • Loading branch information
Simen Mailund Svendsen authored and bim9262 committed Oct 21, 2024
1 parent 005233a commit b77f328
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 16 deletions.
63 changes: 49 additions & 14 deletions src/blocks/weather.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,20 @@
//!
//! # Available Format Keys
//!
//! Key | Value | Type | Unit
//! ---------------------------------------------|-------------------------------------------------------------------------------|--------|-----
//! `location` | Location name (exact format depends on the service) | Text | -
//! `icon{,_ffin}` | Icon representing the weather | Icon | -
//! `weather{,_ffin}` | Textual brief description of the weather, e.g. "Raining" | Text | -
//! `weather_verbose{,_ffin}` | Textual verbose description of the weather, e.g. "overcast clouds" | Text | -
//! `temp{,_{favg,fmin,fmax,ffin}}` | Temperature | Number | degrees
//! `apparent{,_{favg,fmin,fmax,ffin}}` | Australian Apparent Temperature | Number | degrees
//! `humidity{,_{favg,fmin,fmax,ffin}}` | Humidity | Number | %
//! `wind{,_{favg,fmin,fmax,ffin}}` | Wind speed | Number | -
//! `wind_kmh{,_{favg,fmin,fmax,ffin}}` | Wind speed. The wind speed in km/h | Number | -
//! `direction{,_{favg,fmin,fmax,ffin}}` | Wind direction, e.g. "NE" | Text | -
//! Key | Value | Type | Unit
//! ---------------------------------------------|-------------------------------------------------------------------------------|----------|-----
//! `location` | Location name (exact format depends on the service) | Text | -
//! `icon{,_ffin}` | Icon representing the weather | Icon | -
//! `weather{,_ffin}` | Textual brief description of the weather, e.g. "Raining" | Text | -
//! `weather_verbose{,_ffin}` | Textual verbose description of the weather, e.g. "overcast clouds" | Text | -
//! `temp{,_{favg,fmin,fmax,ffin}}` | Temperature | Number | degrees
//! `apparent{,_{favg,fmin,fmax,ffin}}` | Australian Apparent Temperature | Number | degrees
//! `humidity{,_{favg,fmin,fmax,ffin}}` | Humidity | Number | %
//! `wind{,_{favg,fmin,fmax,ffin}}` | Wind speed | Number | -
//! `wind_kmh{,_{favg,fmin,fmax,ffin}}` | Wind speed. The wind speed in km/h | Number | -
//! `direction{,_{favg,fmin,fmax,ffin}}` | Wind direction, e.g. "NE" | Text | -
//! `sunrise` | Time of sunrise | DateTime | -
//! `sunset` | Time of sunset | DateTime | -
//!
//! You can use the suffixes noted above to get the following:
//!
Expand All @@ -97,7 +99,7 @@
//! ----------------|-------------------------------------------|---------------
//! `toggle_format` | Toggles between `format` and `format_alt` | Left
//!
//! # Example
//! # Examples
//!
//! Show detailed weather in San Francisco through the OpenWeatherMap service:
//!
Expand All @@ -114,6 +116,17 @@
//! forecast_hours = 9
//! ```
//!
//! Show sunrise and sunset times in null island
//!
//! ```toml
//! [[block]]
//! block = "weather"
//! format = "up $sunrise.datetime(f:'%R') down $sunset.datetime(f:'%R')"
//! [block.service]
//! name = "metno"
//! coordinates = ["0", "0"]
//! ```
//!
//! # Used Icons
//!
//! - `weather_sun` (when weather is reported as "Clear" during the day)
Expand All @@ -132,6 +145,8 @@ use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::Instant;

use chrono::{DateTime, Utc};

use crate::formatting::Format;

use super::prelude::*;
Expand Down Expand Up @@ -168,6 +183,7 @@ trait WeatherProvider {
&self,
autolocated_location: Option<&Coordinates>,
need_forecast: bool,
need_sunrise_and_sunset: bool,
) -> Result<WeatherResult>;
}

Expand Down Expand Up @@ -255,6 +271,8 @@ struct WeatherResult {
location: String,
current_weather: WeatherMoment,
forecast: Option<Forecast>,
sunrise: Option<DateTime<Utc>>,
sunset: Option<DateTime<Utc>>,
}

impl WeatherResult {
Expand Down Expand Up @@ -301,6 +319,14 @@ impl WeatherResult {
"weather_verbose_ffin" => Value::text(forecast.fin.weather_verbose.clone()),
}
}

if let Some(sunset) = self.sunset {
values.insert("sunset".into(), Value::datetime(sunset, None));
}

if let Some(sunrise) = self.sunrise {
values.insert("sunrise".into(), Value::datetime(sunrise, None));
}
values
}
}
Expand Down Expand Up @@ -439,6 +465,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {

let autolocate_interval = config.autolocate_interval.unwrap_or(config.interval);
let need_forecast = need_forecast(&format, format_alt.as_ref());
let need_sunrise_and_sunset = need_sunrise_and_sunset(&format, format_alt.as_ref());

let mut timer = config.interval.timer();

Expand All @@ -450,7 +477,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
None
};

let fetch = || provider.get_weather(location.as_ref(), need_forecast);
let fetch =
|| provider.get_weather(location.as_ref(), need_forecast, need_sunrise_and_sunset);
let data = fetch.retry(ExponentialBuilder::default()).await?;
let data_values = data.into_values();

Expand Down Expand Up @@ -499,6 +527,13 @@ fn need_forecast(format: &Format, format_alt: Option<&Format>) -> bool {
has_forecast_key(format) || format_alt.is_some_and(has_forecast_key)
}

fn need_sunrise_and_sunset(format: &Format, format_alt: Option<&Format>) -> bool {
fn has_sunrise_or_sunset_keys(format: &Format) -> bool {
format.contains_key("sunrise") || format.contains_key("sunset")
}
has_sunrise_or_sunset_keys(format) || format_alt.is_some_and(has_sunrise_or_sunset_keys)
}

#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, SmartDefault)]
#[serde(rename_all = "lowercase")]
enum UnitSystem {
Expand Down
59 changes: 57 additions & 2 deletions src/blocks/weather/met_no.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,35 @@ struct ForecastTimeInstant {
relative_humidity: Option<f64>,
}

#[derive(Debug, Deserialize)]
struct SunResponse {
properties: Properties,
}

#[derive(Debug, Deserialize)]
struct Properties {
sunrise: TimeData,
sunset: TimeData,
}

#[derive(Debug, Deserialize)]
struct TimeData {
time: String,
}

static LEGENDS: LazyLock<Option<LegendsStore>> =
LazyLock::new(|| serde_json::from_str(include_str!("met_no_legends.json")).ok());

const FORECAST_URL: &str = "https://api.met.no/weatherapi/locationforecast/2.0/compact";
const SUN_URL: &str = "https://api.met.no/weatherapi/sunrise/3.0/sun";

#[async_trait]
impl WeatherProvider for Service<'_> {
async fn get_weather(
&self,
location: Option<&Coordinates>,
need_forecast: bool,
need_sunrise_and_sunset: bool,
) -> Result<WeatherResult> {
let (lat, lon) = location
.as_ref()
Expand All @@ -190,8 +208,8 @@ impl WeatherProvider for Service<'_> {
.error("No location given")?;

let querystr: HashMap<&str, String> = map! {
"lat" => lat,
"lon" => lon,
"lat" => &lat,
"lon" => &lon,
[if let Some(alt) = &self.config.altitude] "altitude" => alt,
};

Expand Down Expand Up @@ -237,10 +255,47 @@ impl WeatherProvider for Service<'_> {

let forecast = Some(Forecast::new(&data_agg, fin));

let (sunset, sunrise) = match need_sunrise_and_sunset {
true => {
let sun_query_string: HashMap<&str, String> = map! {
"lat" => &lat,
"lon" => &lon,
};

let sun_data: SunResponse = REQWEST_CLIENT
.get(SUN_URL)
.query(&sun_query_string)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.send()
.await
.error("Forecast request failed")?
.json()
.await
.error("Forecast request failed")?;

let sunset = Some(
DateTime::parse_from_str(&sun_data.properties.sunset.time, "%Y-%m-%dT%H:%M%z")
.error("failed to parse sunset timestring")?
.to_utc(),
);

let sunrise = Some(
DateTime::parse_from_str(&sun_data.properties.sunrise.time, "%Y-%m-%dT%H:%M%z")
.error("failed to parse sunrise timestring")?
.to_utc(),
);

(sunset, sunrise)
}
false => (None, None),
};

Ok(WeatherResult {
location: location_name,
current_weather,
forecast,
sunset,
sunrise,
})
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/blocks/weather/open_weather_map.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use chrono::{DateTime, Utc};
use serde::{de, Deserializer};

pub(super) const GEO_URL: &str = "https://api.openweathermap.org/geo/1.0";
Expand Down Expand Up @@ -245,6 +246,7 @@ impl WeatherProvider for Service<'_> {
&self,
autolocated: Option<&Coordinates>,
need_forecast: bool,
_need_sunrise_and_sunset: bool,
) -> Result<WeatherResult> {
let location_query = autolocated
.as_ref()
Expand Down Expand Up @@ -318,10 +320,22 @@ impl WeatherProvider for Service<'_> {

let forecast = Some(Forecast::new(&data_agg, fin));

let sunrise = Some(
DateTime::<Utc>::from_timestamp(current_data.sys.sunrise, 0)
.error("Unable to convert timestamp to DateTime")?,
);

let sunset = Some(
DateTime::<Utc>::from_timestamp(current_data.sys.sunset, 0)
.error("Unable to convert timestamp to DateTime")?,
);

Ok(WeatherResult {
location: current_data.name,
current_weather,
forecast,
sunrise,
sunset,
})
}
}
Expand Down

0 comments on commit b77f328

Please sign in to comment.