1use chrono::{DateTime, Utc};
150use sunrise::{SolarDay, SolarEvent};
151
152use super::prelude::*;
153use crate::formatting::Format;
154pub(super) use crate::geolocator::IPAddressInfo;
155use crate::util::{celsius_to_fahrenheit, kmh_to_mph, kmh_to_mps};
156
157pub mod met_no;
158pub mod nws;
159pub mod open_weather_map;
160
161#[derive(Deserialize, Debug)]
162#[serde(deny_unknown_fields)]
163pub struct Config {
164 #[serde(default = "default_interval")]
165 pub interval: Seconds,
166 #[serde(default)]
167 pub format: FormatConfig,
168 pub format_alt: Option<FormatConfig>,
169 pub service: WeatherService,
170 #[serde(default)]
171 pub autolocate: bool,
172 pub autolocate_interval: Option<Seconds>,
173 pub units: Option<UnitSystem>,
174}
175
176fn default_interval() -> Seconds {
177 Seconds::new(600)
178}
179
180#[async_trait]
181trait WeatherProvider {
182 async fn get_weather(
183 &self,
184 autolocated_location: Option<&IPAddressInfo>,
185 need_forecast: bool,
186 ) -> Result<WeatherResult>;
187}
188
189#[derive(Deserialize, Debug)]
190#[serde(tag = "name", rename_all = "lowercase")]
191pub enum WeatherService {
192 OpenWeatherMap(open_weather_map::Config),
193 MetNo(met_no::Config),
194 Nws(nws::Config),
195}
196
197#[derive(Clone, Copy, Default)]
198enum WeatherIcon {
199 Clear {
200 is_night: bool,
201 },
202 Clouds {
203 is_night: bool,
204 },
205 Fog {
206 is_night: bool,
207 },
208 Rain {
209 is_night: bool,
210 },
211 Snow,
212 Thunder {
213 is_night: bool,
214 },
215 #[default]
216 Default,
217}
218
219impl WeatherIcon {
220 fn to_icon_str(self) -> &'static str {
221 match self {
222 Self::Clear { is_night: false } => "weather_sun",
223 Self::Clear { is_night: true } => "weather_moon",
224 Self::Clouds { is_night: false } => "weather_clouds",
225 Self::Clouds { is_night: true } => "weather_clouds_night",
226 Self::Fog { is_night: false } => "weather_fog",
227 Self::Fog { is_night: true } => "weather_fog_night",
228 Self::Rain { is_night: false } => "weather_rain",
229 Self::Rain { is_night: true } => "weather_rain_night",
230 Self::Snow => "weather_snow",
231 Self::Thunder { is_night: false } => "weather_thunder",
232 Self::Thunder { is_night: true } => "weather_thunder_night",
233 Self::Default => "weather_default",
234 }
235 }
236}
237
238#[derive(Default)]
239struct WeatherMoment {
240 icon: WeatherIcon,
241 weather: String,
242 weather_verbose: String,
243 temp: f64,
244 apparent: f64,
245 humidity: f64,
246 wind_kmh: f64,
247 wind_direction: Option<f64>,
248}
249
250struct ForecastAggregate {
251 temp: f64,
252 apparent: f64,
253 humidity: f64,
254 wind_kmh: f64,
255 wind_direction: Option<f64>,
256}
257
258struct ForecastAggregateSegment {
259 temp: Option<f64>,
260 apparent: Option<f64>,
261 humidity: Option<f64>,
262 wind_kmh: Option<f64>,
263 wind_direction: Option<f64>,
264}
265
266struct WeatherResult {
267 location: String,
268 current_weather: WeatherMoment,
269 forecast: Option<Forecast>,
270 sunrise: Option<DateTime<Utc>>,
271 sunset: Option<DateTime<Utc>>,
272}
273
274impl WeatherResult {
275 fn into_values(self, unit_system: &UnitSystem) -> Values {
276 let mut values = map! {
277 "location" => Value::text(self.location),
278 "icon" => Value::icon(self.current_weather.icon.to_icon_str()),
280 "temp" => unit_system.temperature_value(self.current_weather.temp),
281 "apparent" => unit_system.temperature_value(self.current_weather.apparent),
282 "humidity" => Value::percents(self.current_weather.humidity),
283 "weather" => Value::text(self.current_weather.weather),
284 "weather_verbose" => Value::text(self.current_weather.weather_verbose),
285 "wind" => unit_system.wind_speed_value(self.current_weather.wind_kmh),
286 "wind_kmh" => Value::number(self.current_weather.wind_kmh),
287 "direction" => Value::text(convert_wind_direction(self.current_weather.wind_direction).into()),
288 [if let Some(sunrise) = self.sunrise] "sunrise" => Value::datetime(sunrise, None),
289 [if let Some(sunset) = self.sunset] "sunset" => Value::datetime(sunset, None),
290 };
291
292 if let Some(forecast) = self.forecast {
293 macro_rules! map_forecasts {
294 ({$($suffix: literal => $src: expr),* $(,)?}) => {
295 map!{ @extend values
296 $(
297 concat!("temp_f", $suffix) => unit_system.temperature_value($src.temp),
298 concat!("apparent_f", $suffix) => unit_system.temperature_value($src.apparent),
299 concat!("humidity_f", $suffix) => Value::percents($src.humidity),
300 concat!("wind_f", $suffix) => unit_system.wind_speed_value($src.wind_kmh),
301 concat!("wind_kmh_f", $suffix) => Value::number($src.wind_kmh),
302 concat!("direction_f", $suffix) => Value::text(convert_wind_direction($src.wind_direction).into()),
303 )*
304 }
305 };
306 }
307 map_forecasts!({
308 "avg" => forecast.avg,
309 "min" => forecast.min,
310 "max" => forecast.max,
311 "fin" => forecast.fin,
312 });
313
314 map! { @extend values
315 "icon_ffin" => Value::icon(forecast.fin.icon.to_icon_str()),
316 "weather_ffin" => Value::text(forecast.fin.weather.clone()),
317 "weather_verbose_ffin" => Value::text(forecast.fin.weather_verbose.clone()),
318 }
319 }
320
321 values
322 }
323}
324
325struct Forecast {
326 avg: ForecastAggregate,
327 min: ForecastAggregate,
328 max: ForecastAggregate,
329 fin: WeatherMoment,
330}
331
332impl Forecast {
333 fn new(data: &[ForecastAggregateSegment], fin: WeatherMoment) -> Self {
334 let mut temp_avg = 0.0;
335 let mut temp_count = 0.0;
336 let mut apparent_avg = 0.0;
337 let mut apparent_count = 0.0;
338 let mut humidity_avg = 0.0;
339 let mut humidity_count = 0.0;
340 let mut wind_kmh_north_avg = 0.0;
341 let mut wind_kmh_east_avg = 0.0;
342 let mut wind_count = 0.0;
343 let mut max = ForecastAggregate {
344 temp: f64::MIN,
345 apparent: f64::MIN,
346 humidity: f64::MIN,
347 wind_kmh: f64::MIN,
348 wind_direction: None,
349 };
350 let mut min = ForecastAggregate {
351 temp: f64::MAX,
352 apparent: f64::MAX,
353 humidity: f64::MAX,
354 wind_kmh: f64::MAX,
355 wind_direction: None,
356 };
357 for val in data {
358 if let Some(temp) = val.temp {
359 temp_avg += temp;
360 max.temp = max.temp.max(temp);
361 min.temp = min.temp.min(temp);
362 temp_count += 1.0;
363 }
364 if let Some(apparent) = val.apparent {
365 apparent_avg += apparent;
366 max.apparent = max.apparent.max(apparent);
367 min.apparent = min.apparent.min(apparent);
368 apparent_count += 1.0;
369 }
370 if let Some(humidity) = val.humidity {
371 humidity_avg += humidity;
372 max.humidity = max.humidity.max(humidity);
373 min.humidity = min.humidity.min(humidity);
374 humidity_count += 1.0;
375 }
376
377 if let Some(wind_kmh) = val.wind_kmh {
378 if let Some(degrees) = val.wind_direction {
379 let (sin, cos) = degrees.to_radians().sin_cos();
380 wind_kmh_north_avg += wind_kmh * cos;
381 wind_kmh_east_avg += wind_kmh * sin;
382 wind_count += 1.0;
383 }
384
385 if wind_kmh > max.wind_kmh {
386 max.wind_direction = val.wind_direction;
387 max.wind_kmh = wind_kmh;
388 }
389
390 if wind_kmh < min.wind_kmh {
391 min.wind_direction = val.wind_direction;
392 min.wind_kmh = wind_kmh;
393 }
394 }
395 }
396
397 temp_avg /= temp_count;
398 humidity_avg /= humidity_count;
399 apparent_avg /= apparent_count;
400
401 let (wind_kmh_avg, wind_direction_avg) = if wind_count == 0.0 {
403 (0.0, None)
404 } else {
405 (
406 wind_kmh_east_avg.hypot(wind_kmh_north_avg) / wind_count,
407 Some(
408 wind_kmh_east_avg
409 .atan2(wind_kmh_north_avg)
410 .to_degrees()
411 .rem_euclid(360.0),
412 ),
413 )
414 };
415
416 let avg = ForecastAggregate {
417 temp: temp_avg,
418 apparent: apparent_avg,
419 humidity: humidity_avg,
420 wind_kmh: wind_kmh_avg,
421 wind_direction: wind_direction_avg,
422 };
423 Self { avg, min, max, fin }
424 }
425}
426
427pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
428 let mut actions = api.get_actions()?;
429 api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
430
431 let mut format = config.format.with_default(" $icon $weather $temp ")?;
432 let mut format_alt = match &config.format_alt {
433 Some(f) => Some(f.with_default("")?),
434 None => None,
435 };
436
437 let (provider, service_units): (Box<dyn WeatherProvider + Send + Sync>, UnitSystem) =
438 match &config.service {
439 WeatherService::MetNo(service_config) => (
440 Box::new(met_no::Service::new(service_config)?),
441 UnitSystem::default(),
442 ),
443 WeatherService::OpenWeatherMap(service_config) => (
444 Box::new(open_weather_map::Service::new(config.autolocate, service_config).await?),
445 service_config.units,
446 ),
447 WeatherService::Nws(service_config) => (
448 Box::new(nws::Service::new(config.autolocate, service_config).await?),
449 service_config.units,
450 ),
451 };
452 let units = config.units.unwrap_or(service_units);
453
454 let autolocate_interval = config.autolocate_interval.unwrap_or(config.interval);
455 let need_forecast = need_forecast(&format, format_alt.as_ref());
456
457 let mut timer = config.interval.timer();
458
459 loop {
460 let location = if config.autolocate {
461 let fetch = || api.find_ip_location(&REQWEST_CLIENT, autolocate_interval.0);
462 Some(fetch.retry(ExponentialBuilder::default()).await?)
463 } else {
464 None
465 };
466
467 let fetch = || provider.get_weather(location.as_ref(), need_forecast);
468 let data = fetch.retry(ExponentialBuilder::default()).await?;
469 let data_values = data.into_values(&units);
470
471 loop {
472 let mut widget = Widget::new().with_format(format.clone());
473 widget.set_values(data_values.clone());
474 api.set_widget(widget)?;
475
476 select! {
477 _ = timer.tick() => break,
478 _ = api.wait_for_update_request() => break,
479 Some(action) = actions.recv() => match action.as_ref() {
480 "toggle_format" => {
481 if let Some(ref mut format_alt) = format_alt {
482 std::mem::swap(format_alt, &mut format);
483 }
484 }
485 _ => (),
486 }
487 }
488 }
489 }
490}
491
492fn need_forecast(format: &Format, format_alt: Option<&Format>) -> bool {
493 fn has_forecast_key(format: &Format) -> bool {
494 macro_rules! format_suffix {
495 ($($suffix: literal),* $(,)?) => {
496 false
497 $(
498 || format.contains_key(concat!("temp_f", $suffix))
499 || format.contains_key(concat!("apparent_f", $suffix))
500 || format.contains_key(concat!("humidity_f", $suffix))
501 || format.contains_key(concat!("wind_f", $suffix))
502 || format.contains_key(concat!("wind_kmh_f", $suffix))
503 || format.contains_key(concat!("direction_f", $suffix))
504 )*
505 };
506 }
507
508 format_suffix!("avg", "min", "max", "fin")
509 || format.contains_key("icon_ffin")
510 || format.contains_key("weather_ffin")
511 || format.contains_key("weather_verbose_ffin")
512 }
513 has_forecast_key(format) || format_alt.is_some_and(has_forecast_key)
514}
515
516fn calculate_sunrise_sunset(
517 lat: f64,
518 lon: f64,
519 altitude: Option<f64>,
520) -> Result<(Option<DateTime<Utc>>, Option<DateTime<Utc>>)> {
521 let date = Utc::now().date_naive();
522 let coordinates = sunrise::Coordinates::new(lat, lon).error("Invalid coordinates")?;
523 let solar_day = SolarDay::new(coordinates, date).with_altitude(altitude.unwrap_or_default());
524
525 Ok((
526 solar_day.event_time(SolarEvent::Sunrise),
527 solar_day.event_time(SolarEvent::Sunset),
528 ))
529}
530
531#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, SmartDefault)]
532#[serde(rename_all = "lowercase")]
533pub enum UnitSystem {
534 #[default]
535 Metric,
536 Imperial,
537}
538
539impl AsRef<str> for UnitSystem {
540 fn as_ref(&self) -> &str {
541 match self {
542 UnitSystem::Metric => "metric",
543 UnitSystem::Imperial => "imperial",
544 }
545 }
546}
547
548impl UnitSystem {
549 fn temperature_value(&self, temp_celsius: f64) -> Value {
550 match self {
551 UnitSystem::Metric => Value::degrees_c(temp_celsius),
552 UnitSystem::Imperial => Value::degrees_f(celsius_to_fahrenheit(temp_celsius)),
553 }
554 }
555
556 fn wind_speed_value(&self, speed_kmh: f64) -> Value {
557 match self {
558 UnitSystem::Metric => Value::number(kmh_to_mps(speed_kmh)),
559 UnitSystem::Imperial => Value::number(kmh_to_mph(speed_kmh)),
560 }
561 }
562}
563
564fn convert_wind_direction(direction_opt: Option<f64>) -> &'static str {
566 match direction_opt {
567 Some(direction) => match direction.round() as i64 {
568 24..=68 => "NE",
569 69..=113 => "E",
570 114..=158 => "SE",
571 159..=203 => "S",
572 204..=248 => "SW",
573 249..=293 => "W",
574 294..=338 => "NW",
575 _ => "N",
576 },
577 None => "-",
578 }
579}
580
581fn australian_apparent_temp(temp: f64, humidity: f64, wind_speed: f64) -> f64 {
583 let exponent = 17.27 * temp / (237.7 + temp);
584 let water_vapor_pressure = humidity * 0.06105 * exponent.exp();
585 temp + 0.33 * water_vapor_pressure - 0.7 * wind_speed - 4.0
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn test_new_forecast_average_wind_speed() {
594 let mut degrees = 0.0;
595 while degrees < 360.0 {
596 let forecast = Forecast::new(
597 &[
598 ForecastAggregateSegment {
599 temp: None,
600 apparent: None,
601 humidity: None,
602 wind_kmh: Some(3.6),
603 wind_direction: Some(degrees),
604 },
605 ForecastAggregateSegment {
606 temp: None,
607 apparent: None,
608 humidity: None,
609 wind_kmh: Some(7.2),
610 wind_direction: Some(degrees),
611 },
612 ],
613 WeatherMoment::default(),
614 );
615 assert!((forecast.avg.wind_kmh - 5.4).abs() < 0.1);
616 assert!((forecast.avg.wind_direction.unwrap() - degrees).abs() < 0.1);
617
618 degrees += 15.0;
619 }
620 }
621
622 #[test]
623 fn test_new_forecast_average_wind_degrees() {
624 let mut degrees = 0.0;
625 while degrees < 360.0 {
626 let low = degrees - 1.0;
627 let high = degrees + 1.0;
628 let forecast = Forecast::new(
629 &[
630 ForecastAggregateSegment {
631 temp: None,
632 apparent: None,
633 humidity: None,
634 wind_kmh: Some(3.6),
635 wind_direction: Some(low),
636 },
637 ForecastAggregateSegment {
638 temp: None,
639 apparent: None,
640 humidity: None,
641 wind_kmh: Some(3.6),
642 wind_direction: Some(high),
643 },
644 ],
645 WeatherMoment::default(),
646 );
647 assert!((forecast.avg.wind_direction.unwrap() - degrees).abs() < 0.1);
650
651 degrees += 15.0;
652 }
653 }
654
655 #[test]
656 fn test_new_forecast_average_wind_speed_and_degrees() {
657 let mut degrees = 0.0;
658 while degrees < 360.0 {
659 let low = degrees - 1.0;
660 let high = degrees + 1.0;
661 let forecast = Forecast::new(
662 &[
663 ForecastAggregateSegment {
664 temp: None,
665 apparent: None,
666 humidity: None,
667 wind_kmh: Some(3.6),
668 wind_direction: Some(low),
669 },
670 ForecastAggregateSegment {
671 temp: None,
672 apparent: None,
673 humidity: None,
674 wind_kmh: Some(7.2),
675 wind_direction: Some(high),
676 },
677 ],
678 WeatherMoment::default(),
679 );
680 assert!((low + high) / 2.0 < forecast.avg.wind_direction.unwrap());
685 assert!(forecast.avg.wind_direction.unwrap() < high);
686 degrees += 15.0;
687 }
688 }
689}