1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: MIT
3
4use crate::ui::*;
5use chrono::prelude::*;
6use core::cmp::Ordering;
7use futures::future;
8use slint::*;
9use std::rc::Rc;
10use weer_api::*;
11
12use std::thread;
13
14const WEATHER_API_KEY: &str = "WEATHER_API";
15const WEATHER_LAT_KEY: &str = "WEATHER_LAT";
16const WEATHER_LONG_KEY: &str = "WEATHER_LONG";
17const LAT_BERLIN: f32 = 52.520008;
18const LONG_BERLIN: f32 = 13.404954;
19const FORECAST_DAYS: i64 = 3;
20
21pub fn setup(window: &MainWindow) -> thread::JoinHandle<()> {
22 let window_weak: Weak = window.as_weak();
23
24 thread::spawn(move || {
25 tokio::runtime::Runtime::new().unwrap().block_on(future:weather_worker_loop(window_weak))
26 })
27}
28
29async fn weather_worker_loop(window_weak: Weak<MainWindow>) {
30 let api_key = api_key();
31 if api_key.is_empty() {
32 return;
33 }
34
35 let lat = lat();
36 let long = long();
37
38 let now = Local::now();
39
40 let mut forecast_days = vec![];
41
42 let client = Client::new(&api_key, true);
43 let mut forecast_list = future::join_all((0..FORECAST_DAYS).map(|i| {
44 let client = client.clone();
45 async move {
46 current_forecast(
47 client.clone(),
48 lat,
49 long,
50 now + chrono::TimeDelta::try_days(i).unwrap_or_default(),
51 )
52 .await
53 }
54 }))
55 .await;
56
57 for i in 0..forecast_list.len() {
58 if let Some((date, forecast)) = forecast_list.remove(0) {
59 if i == 1 {
60 display_current(
61 window_weak.clone(),
62 forecast.current,
63 SharedString::from(now.format("%e %B %Y").to_string()),
64 );
65 }
66
67 {
68 let forecast = forecast.forecast;
69 let mut day = forecast.forecast_day;
70
71 // the api provides only one day in the forecast therefore an iteration is necessary to get all.
72 if !day.is_empty() {
73 forecast_days.push((day.remove(0), date.format("%A").to_string()));
74 }
75 }
76 }
77 }
78
79 if !forecast_days.is_empty() {
80 display_forecast(window_weak.clone(), forecast_days);
81 }
82}
83
84async fn current_forecast(
85 client: Client,
86 lat: f32,
87 long: f32,
88 date: DateTime<Local>,
89) -> Option<(DateTime<Local>, Forecast)> {
90 if let Ok(forecast: Forecast) = client.forecast().query(Query::Coords(lat, long)).dt(date).call() {
91 return Some((date, forecast));
92 }
93
94 None
95}
96
97fn display_current(window_weak: Weak<MainWindow>, current: Current, current_date: SharedString) {
98 window_weakResult<(), EventLoopError>
99 .upgrade_in_event_loop(func:move |window: MainWindow| {
100 windowWeatherAdapter<'_>
101 .global::<WeatherAdapter>()
102 .set_current_temperature(SharedString::from(current.temp_c.to_string()));
103 window.global::<WeatherAdapter>().set_current_day(current_date);
104 window.global::<WeatherAdapter>().set_current_weather_description(SharedString::from(
105 current.condition.text.to_string(),
106 ));
107 windowWeatherAdapter<'_>
108 .global::<WeatherAdapter>()
109 .set_current_temperature_icon(get_icon(&window, &current.condition));
110 })
111 .unwrap();
112}
113
114fn display_forecast(window_weak: Weak<MainWindow>, forecast: Vec<(ForecastDay, String)>) {
115 window_weak
116 .upgrade_in_event_loop(move |window| {
117 let forecast_model = VecModel::default();
118
119 let max_temp = forecast
120 .iter()
121 .max_by(|lhs, rhs| {
122 if lhs.0.day.temp_c().max() > rhs.0.day.temp_c().max() {
123 Ordering::Greater
124 } else {
125 Ordering::Less
126 }
127 })
128 .map(|d| d.0.day.temp_c().max())
129 .unwrap_or_default();
130
131 let min_temp = forecast
132 .iter()
133 .min_by(|lhs, rhs| {
134 if lhs.0.day.temp_c().min() > rhs.0.day.temp_c().min() {
135 Ordering::Greater
136 } else {
137 Ordering::Less
138 }
139 })
140 .map(|d| d.0.day.temp_c().min())
141 .unwrap_or_default();
142
143 for (forecast_day, day) in forecast {
144 let model = BarTileModel {
145 title: SharedString::from(&day.as_str()[0..3]),
146 max: forecast_day.day.temp_c().max().round() as i32,
147 min: forecast_day.day.temp_c().min().round() as i32,
148 absolute_max: max_temp.round() as i32,
149 absolute_min: min_temp.round() as i32,
150 unit: SharedString::from("°"),
151 icon: get_icon(&window, &forecast_day.day.condition),
152 };
153
154 forecast_model.push(model);
155 }
156
157 window.global::<WeatherAdapter>().set_week_model(Rc::new(forecast_model).into());
158 })
159 .unwrap();
160}
161
162fn get_icon(window: &MainWindow, condition: &Condition) -> Image {
163 // code mapping can be found at https://www.weatherapi.com/docs/conditions.json
164 match condition.code {
165 1003 => window.global::<Images>().get_cloudy(),
166 1006 => window.global::<Images>().get_cloud(),
167 _ => window.global::<Images>().get_sunny(),
168 }
169}
170
171fn api_key() -> String {
172 if let Some(lat: &str) = option_env!("WEATHER_API") {
173 return lat.to_string();
174 }
175
176 #[cfg(not(feature = "mcu-board-support"))]
177 if let Some(lat: OsString) = std::env::var_os(WEATHER_API_KEY) {
178 if let Some(lat: &str) = lat.to_str() {
179 return lat.to_string();
180 }
181 }
182
183 String::default()
184}
185
186fn lat() -> f32 {
187 if let Some(lat: &str) = option_env!("WEATHER_LAT") {
188 return lat.parse().unwrap_or_default();
189 }
190
191 #[cfg(not(feature = "mcu-board-support"))]
192 if let Some(lat: OsString) = std::env::var_os(WEATHER_LAT_KEY) {
193 if let Some(lat: &str) = lat.to_str() {
194 return lat.parse().unwrap_or_default();
195 }
196 }
197
198 LAT_BERLIN
199}
200
201fn long() -> f32 {
202 if let Some(lat: &str) = option_env!("WEATHER_LONG") {
203 return lat.parse().unwrap_or_default();
204 }
205
206 #[cfg(not(feature = "mcu-board-support"))]
207 if let Some(lat: OsString) = std::env::var_os(WEATHER_LONG_KEY) {
208 if let Some(lat: &str) = lat.to_str() {
209 return lat.parse().unwrap_or_default();
210 }
211 }
212
213 LONG_BERLIN
214}
215