use chrono::{DateTime, Utc};
use sunrise::{SolarDay, SolarEvent};
use crate::formatting::Format;
pub(super) use crate::geolocator::IPAddressInfo;
use super::prelude::*;
pub mod met_no;
pub mod nws;
pub mod open_weather_map;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default = "default_interval")]
pub interval: Seconds,
#[serde(default)]
pub format: FormatConfig,
pub format_alt: Option<FormatConfig>,
pub service: WeatherService,
#[serde(default)]
pub autolocate: bool,
pub autolocate_interval: Option<Seconds>,
}
fn default_interval() -> Seconds {
Seconds::new(600)
}
#[async_trait]
trait WeatherProvider {
async fn get_weather(
&self,
autolocated_location: Option<&IPAddressInfo>,
need_forecast: bool,
) -> Result<WeatherResult>;
}
#[derive(Deserialize, Debug)]
#[serde(tag = "name", rename_all = "lowercase")]
pub enum WeatherService {
OpenWeatherMap(open_weather_map::Config),
MetNo(met_no::Config),
Nws(nws::Config),
}
#[derive(Clone, Copy, Default)]
enum WeatherIcon {
Clear {
is_night: bool,
},
Clouds {
is_night: bool,
},
Fog {
is_night: bool,
},
Rain {
is_night: bool,
},
Snow,
Thunder {
is_night: bool,
},
#[default]
Default,
}
impl WeatherIcon {
fn to_icon_str(self) -> &'static str {
match self {
Self::Clear { is_night: false } => "weather_sun",
Self::Clear { is_night: true } => "weather_moon",
Self::Clouds { is_night: false } => "weather_clouds",
Self::Clouds { is_night: true } => "weather_clouds_night",
Self::Fog { is_night: false } => "weather_fog",
Self::Fog { is_night: true } => "weather_fog_night",
Self::Rain { is_night: false } => "weather_rain",
Self::Rain { is_night: true } => "weather_rain_night",
Self::Snow => "weather_snow",
Self::Thunder { is_night: false } => "weather_thunder",
Self::Thunder { is_night: true } => "weather_thunder_night",
Self::Default => "weather_default",
}
}
}
#[derive(Default)]
struct WeatherMoment {
icon: WeatherIcon,
weather: String,
weather_verbose: String,
temp: f64,
apparent: f64,
humidity: f64,
wind: f64,
wind_kmh: f64,
wind_direction: Option<f64>,
}
struct ForecastAggregate {
temp: f64,
apparent: f64,
humidity: f64,
wind: f64,
wind_kmh: f64,
wind_direction: Option<f64>,
}
struct ForecastAggregateSegment {
temp: Option<f64>,
apparent: Option<f64>,
humidity: Option<f64>,
wind: Option<f64>,
wind_kmh: Option<f64>,
wind_direction: Option<f64>,
}
struct WeatherResult {
location: String,
current_weather: WeatherMoment,
forecast: Option<Forecast>,
sunrise: Option<DateTime<Utc>>,
sunset: Option<DateTime<Utc>>,
}
impl WeatherResult {
fn into_values(self) -> Values {
let mut values = map! {
"location" => Value::text(self.location),
"icon" => Value::icon(self.current_weather.icon.to_icon_str()),
"temp" => Value::degrees(self.current_weather.temp),
"apparent" => Value::degrees(self.current_weather.apparent),
"humidity" => Value::percents(self.current_weather.humidity),
"weather" => Value::text(self.current_weather.weather),
"weather_verbose" => Value::text(self.current_weather.weather_verbose),
"wind" => Value::number(self.current_weather.wind),
"wind_kmh" => Value::number(self.current_weather.wind_kmh),
"direction" => Value::text(convert_wind_direction(self.current_weather.wind_direction).into()),
[if let Some(sunrise) = self.sunrise] "sunrise" => Value::datetime(sunrise, None),
[if let Some(sunset) = self.sunset] "sunset" => Value::datetime(sunset, None),
};
if let Some(forecast) = self.forecast {
macro_rules! map_forecasts {
({$($suffix: literal => $src: expr),* $(,)?}) => {
map!{ @extend values
$(
concat!("temp_f", $suffix) => Value::degrees($src.temp),
concat!("apparent_f", $suffix) => Value::degrees($src.apparent),
concat!("humidity_f", $suffix) => Value::percents($src.humidity),
concat!("wind_f", $suffix) => Value::number($src.wind),
concat!("wind_kmh_f", $suffix) => Value::number($src.wind_kmh),
concat!("direction_f", $suffix) => Value::text(convert_wind_direction($src.wind_direction).into()),
)*
}
};
}
map_forecasts!({
"avg" => forecast.avg,
"min" => forecast.min,
"max" => forecast.max,
"fin" => forecast.fin,
});
map! { @extend values
"icon_ffin" => Value::icon(forecast.fin.icon.to_icon_str()),
"weather_ffin" => Value::text(forecast.fin.weather.clone()),
"weather_verbose_ffin" => Value::text(forecast.fin.weather_verbose.clone()),
}
}
values
}
}
struct Forecast {
avg: ForecastAggregate,
min: ForecastAggregate,
max: ForecastAggregate,
fin: WeatherMoment,
}
impl Forecast {
fn new(data: &[ForecastAggregateSegment], fin: WeatherMoment) -> Self {
let mut temp_avg = 0.0;
let mut temp_count = 0.0;
let mut apparent_avg = 0.0;
let mut apparent_count = 0.0;
let mut humidity_avg = 0.0;
let mut humidity_count = 0.0;
let mut wind_north_avg = 0.0;
let mut wind_east_avg = 0.0;
let mut wind_kmh_north_avg = 0.0;
let mut wind_kmh_east_avg = 0.0;
let mut wind_count = 0.0;
let mut max = ForecastAggregate {
temp: f64::MIN,
apparent: f64::MIN,
humidity: f64::MIN,
wind: f64::MIN,
wind_kmh: f64::MIN,
wind_direction: None,
};
let mut min = ForecastAggregate {
temp: f64::MAX,
apparent: f64::MAX,
humidity: f64::MAX,
wind: f64::MAX,
wind_kmh: f64::MAX,
wind_direction: None,
};
for val in data {
if let Some(temp) = val.temp {
temp_avg += temp;
max.temp = max.temp.max(temp);
min.temp = min.temp.min(temp);
temp_count += 1.0;
}
if let Some(apparent) = val.apparent {
apparent_avg += apparent;
max.apparent = max.apparent.max(apparent);
min.apparent = min.apparent.min(apparent);
apparent_count += 1.0;
}
if let Some(humidity) = val.humidity {
humidity_avg += humidity;
max.humidity = max.humidity.max(humidity);
min.humidity = min.humidity.min(humidity);
humidity_count += 1.0;
}
if let Some(wind) = val.wind
&& let Some(wind_kmh) = val.wind_kmh
{
if let Some(degrees) = val.wind_direction {
let (sin, cos) = degrees.to_radians().sin_cos();
wind_north_avg += wind * cos;
wind_east_avg += wind * sin;
wind_kmh_north_avg += wind_kmh * cos;
wind_kmh_east_avg += wind_kmh * sin;
wind_count += 1.0;
}
if wind > max.wind {
max.wind_direction = val.wind_direction;
max.wind = wind;
max.wind_kmh = wind_kmh;
}
if wind < min.wind {
min.wind_direction = val.wind_direction;
min.wind = wind;
min.wind_kmh = wind_kmh;
}
}
}
temp_avg /= temp_count;
humidity_avg /= humidity_count;
apparent_avg /= apparent_count;
let (wind_avg, wind_kmh_avg, wind_direction_avg) = if wind_count == 0.0 {
(0.0, 0.0, None)
} else {
(
wind_east_avg.hypot(wind_north_avg) / wind_count,
wind_kmh_east_avg.hypot(wind_kmh_north_avg) / wind_count,
Some(
wind_east_avg
.atan2(wind_north_avg)
.to_degrees()
.rem_euclid(360.0),
),
)
};
let avg = ForecastAggregate {
temp: temp_avg,
apparent: apparent_avg,
humidity: humidity_avg,
wind: wind_avg,
wind_kmh: wind_kmh_avg,
wind_direction: wind_direction_avg,
};
Self { avg, min, max, fin }
}
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
let mut format = config.format.with_default(" $icon $weather $temp ")?;
let mut format_alt = match &config.format_alt {
Some(f) => Some(f.with_default("")?),
None => None,
};
let provider: Box<dyn WeatherProvider + Send + Sync> = match &config.service {
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?)
}
WeatherService::Nws(service_config) => {
Box::new(nws::Service::new(config.autolocate, service_config).await?)
}
};
let autolocate_interval = config.autolocate_interval.unwrap_or(config.interval);
let need_forecast = need_forecast(&format, format_alt.as_ref());
let mut timer = config.interval.timer();
loop {
let location = if config.autolocate {
let fetch = || api.find_ip_location(&REQWEST_CLIENT, autolocate_interval.0);
Some(fetch.retry(ExponentialBuilder::default()).await?)
} else {
None
};
let fetch = || provider.get_weather(location.as_ref(), need_forecast);
let data = fetch.retry(ExponentialBuilder::default()).await?;
let data_values = data.into_values();
loop {
let mut widget = Widget::new().with_format(format.clone());
widget.set_values(data_values.clone());
api.set_widget(widget)?;
select! {
_ = timer.tick() => break,
_ = api.wait_for_update_request() => break,
Some(action) = actions.recv() => match action.as_ref() {
"toggle_format" => {
if let Some(ref mut format_alt) = format_alt {
std::mem::swap(format_alt, &mut format);
}
}
_ => (),
}
}
}
}
}
fn need_forecast(format: &Format, format_alt: Option<&Format>) -> bool {
fn has_forecast_key(format: &Format) -> bool {
macro_rules! format_suffix {
($($suffix: literal),* $(,)?) => {
false
$(
|| format.contains_key(concat!("temp_f", $suffix))
|| format.contains_key(concat!("apparent_f", $suffix))
|| format.contains_key(concat!("humidity_f", $suffix))
|| format.contains_key(concat!("wind_f", $suffix))
|| format.contains_key(concat!("wind_kmh_f", $suffix))
|| format.contains_key(concat!("direction_f", $suffix))
)*
};
}
format_suffix!("avg", "min", "max", "fin")
|| format.contains_key("icon_ffin")
|| format.contains_key("weather_ffin")
|| format.contains_key("weather_verbose_ffin")
}
has_forecast_key(format) || format_alt.is_some_and(has_forecast_key)
}
fn calculate_sunrise_sunset(
lat: f64,
lon: f64,
altitude: Option<f64>,
) -> Result<(Option<DateTime<Utc>>, Option<DateTime<Utc>>)> {
let date = Utc::now().date_naive();
let coordinates = sunrise::Coordinates::new(lat, lon).error("Invalid coordinates")?;
let solar_day = SolarDay::new(coordinates, date).with_altitude(altitude.unwrap_or_default());
Ok((
solar_day.event_time(SolarEvent::Sunrise),
solar_day.event_time(SolarEvent::Sunset),
))
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, SmartDefault)]
#[serde(rename_all = "lowercase")]
enum UnitSystem {
#[default]
Metric,
Imperial,
}
impl AsRef<str> for UnitSystem {
fn as_ref(&self) -> &str {
match self {
UnitSystem::Metric => "metric",
UnitSystem::Imperial => "imperial",
}
}
}
fn convert_wind_direction(direction_opt: Option<f64>) -> &'static str {
match direction_opt {
Some(direction) => match direction.round() as i64 {
24..=68 => "NE",
69..=113 => "E",
114..=158 => "SE",
159..=203 => "S",
204..=248 => "SW",
249..=293 => "W",
294..=338 => "NW",
_ => "N",
},
None => "-",
}
}
fn australian_apparent_temp(temp: f64, humidity: f64, wind_speed: f64) -> f64 {
let exponent = 17.27 * temp / (237.7 + temp);
let water_vapor_pressure = humidity * 0.06105 * exponent.exp();
temp + 0.33 * water_vapor_pressure - 0.7 * wind_speed - 4.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_forecast_average_wind_speed() {
let mut degrees = 0.0;
while degrees < 360.0 {
let forecast = Forecast::new(
&[
ForecastAggregateSegment {
temp: None,
apparent: None,
humidity: None,
wind: Some(1.0),
wind_kmh: Some(3.6),
wind_direction: Some(degrees),
},
ForecastAggregateSegment {
temp: None,
apparent: None,
humidity: None,
wind: Some(2.0),
wind_kmh: Some(7.2),
wind_direction: Some(degrees),
},
],
WeatherMoment::default(),
);
assert!((forecast.avg.wind - 1.5).abs() < 0.1);
assert!((forecast.avg.wind_kmh - 5.4).abs() < 0.1);
assert!((forecast.avg.wind_direction.unwrap() - degrees).abs() < 0.1);
degrees += 15.0;
}
}
#[test]
fn test_new_forecast_average_wind_degrees() {
let mut degrees = 0.0;
while degrees < 360.0 {
let low = degrees - 1.0;
let high = degrees + 1.0;
let forecast = Forecast::new(
&[
ForecastAggregateSegment {
temp: None,
apparent: None,
humidity: None,
wind: Some(1.0),
wind_kmh: Some(3.6),
wind_direction: Some(low),
},
ForecastAggregateSegment {
temp: None,
apparent: None,
humidity: None,
wind: Some(1.0),
wind_kmh: Some(3.6),
wind_direction: Some(high),
},
],
WeatherMoment::default(),
);
assert!((forecast.avg.wind_direction.unwrap() - degrees).abs() < 0.1);
degrees += 15.0;
}
}
#[test]
fn test_new_forecast_average_wind_speed_and_degrees() {
let mut degrees = 0.0;
while degrees < 360.0 {
let low = degrees - 1.0;
let high = degrees + 1.0;
let forecast = Forecast::new(
&[
ForecastAggregateSegment {
temp: None,
apparent: None,
humidity: None,
wind: Some(1.0),
wind_kmh: Some(3.6),
wind_direction: Some(low),
},
ForecastAggregateSegment {
temp: None,
apparent: None,
humidity: None,
wind: Some(2.0),
wind_kmh: Some(7.2),
wind_direction: Some(high),
},
],
WeatherMoment::default(),
);
assert!((low + high) / 2.0 < forecast.avg.wind_direction.unwrap());
assert!(forecast.avg.wind_direction.unwrap() < high);
degrees += 15.0;
}
}
}