1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: MIT |
3 | |
4 | use crate::ui::*; |
5 | use chrono::prelude::*; |
6 | use core::cmp::Ordering; |
7 | use futures::future; |
8 | use slint::*; |
9 | use std::rc::Rc; |
10 | use weer_api::*; |
11 | |
12 | use std::thread; |
13 | |
14 | const WEATHER_API_KEY: &str = "WEATHER_API" ; |
15 | const WEATHER_LAT_KEY: &str = "WEATHER_LAT" ; |
16 | const WEATHER_LONG_KEY: &str = "WEATHER_LONG" ; |
17 | const LAT_BERLIN: f32 = 52.520008; |
18 | const LONG_BERLIN: f32 = 13.404954; |
19 | const FORECAST_DAYS: i64 = 3; |
20 | |
21 | pub 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 | |
29 | async 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 | |
84 | async 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 | |
97 | fn 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, ¤t.condition)); |
110 | }) |
111 | .unwrap(); |
112 | } |
113 | |
114 | fn 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 | |
162 | fn 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 | |
171 | fn 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 | |
186 | fn 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 | |
201 | fn 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 | |