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