1use serde::{Deserialize, Serialize};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4#[derive(Debug, Clone)]
6pub struct Credentials {
7 pub access_token: String,
8 pub refresh_token: String,
9 pub username: String,
10 pub installation_key: Option<crate::installation_key::InstallationKey>,
12}
13
14#[derive(Debug, Deserialize, Clone)]
15pub struct Machine {
16 #[serde(rename = "serialNumber")]
17 pub serial_number: String,
18 #[serde(rename = "modelName")]
19 pub model: Option<String>,
20 pub name: Option<String>,
21 #[allow(dead_code)] pub location: Option<String>,
23 pub connected: bool,
24}
25
26#[derive(Debug, Deserialize)]
27pub struct MachinesResponse {
28 pub things: Vec<Machine>,
29}
30
31#[derive(Debug, Serialize)]
32pub struct MachineCommand {
33 pub mode: String,
34}
35
36#[derive(Debug, Deserialize)]
37pub struct MachineStatus {
38 pub widgets: Vec<Widget>,
39}
40
41#[derive(Debug, Deserialize)]
42pub struct Widget {
43 pub code: String,
44 pub output: Option<WidgetOutput>,
45}
46
47#[derive(Debug, Deserialize)]
48pub struct WidgetOutput {
49 pub status: Option<String>,
50 #[allow(dead_code)]
51 pub mode: Option<String>,
52 #[serde(rename = "readyStartTime")]
54 pub ready_start_time: Option<u64>,
55}
56
57impl MachineStatus {
58 pub fn is_on(&self) -> bool {
59 for widget in &self.widgets {
61 if widget.code == "CMMachineStatus" {
62 if let Some(output) = &widget.output {
63 if let Some(status) = &output.status {
64 return status != "StandBy";
65 }
66 }
67 }
68 }
69 false }
71
72 pub fn get_status_string(&self) -> String {
73 self.get_status_string_with_time(None)
74 }
75
76 pub fn get_status_string_with_time(&self, current_time_ms: Option<u64>) -> String {
77 let mut is_powered_on = false;
79 for widget in &self.widgets {
80 if widget.code == "CMMachineStatus" {
81 if let Some(output) = &widget.output {
82 if let Some(status) = &output.status {
83 match status.as_str() {
84 "StandBy" => return "Standby".to_string(),
85 "PoweredOn" => {
86 is_powered_on = true;
87 break;
88 }
89 _ => return status.clone(),
90 }
91 }
92 }
93 }
94 }
95
96 if !is_powered_on {
97 return "Unknown".to_string();
98 }
99
100 for widget in &self.widgets {
102 if widget.code == "CMCoffeeBoiler" {
103 if let Some(output) = &widget.output {
104 if let Some(status) = &output.status {
105 if status == "Ready" {
106 return "On (Ready)".to_string();
107 } else if status == "NoWater" {
108 return "On (No water)".to_string();
109 } else if let Some(ready_time) = output.ready_start_time {
110 let now = current_time_ms.unwrap_or_else(|| {
111 SystemTime::now()
112 .duration_since(UNIX_EPOCH)
113 .unwrap()
114 .as_millis() as u64
115 });
116
117 if ready_time > now {
118 let seconds_remaining = (ready_time - now) / 1000;
119 let minutes_remaining = seconds_remaining / 60;
120
121 if minutes_remaining == 0 {
122 return "On (Ready in < 1 min)".to_string();
123 } else if minutes_remaining == 1 {
124 return "On (Ready in 1 min)".to_string();
125 } else {
126 return format!("On (Ready in {} mins)", minutes_remaining);
127 }
128 } else {
129 return "On (Ready in < 1 min)".to_string();
131 }
132 } else {
133 return "On (Ready soon)".to_string();
135 }
136 }
137 }
138 }
139 }
140
141 "On".to_string()
143 }
144}
145
146impl Machine {
147 pub async fn get_status_display(&self, client: &crate::client::LaMarzoccoClient) -> String {
148 if !self.connected {
149 return "Unavailable".to_string();
150 }
151
152 match client.get_machine_status(&self.serial_number).await {
153 Ok(status) => status.get_status_string(),
154 Err(_) => "Unknown".to_string(),
155 }
156 }
157}
158
159impl MachineCommand {
160 pub fn turn_on() -> Self {
161 Self {
162 mode: "BrewingMode".to_string(),
163 }
164 }
165
166 pub fn turn_off() -> Self {
167 Self {
168 mode: "StandBy".to_string(),
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_machine_command_creation() {
179 let on_command = MachineCommand::turn_on();
180 assert_eq!(on_command.mode, "BrewingMode");
181
182 let off_command = MachineCommand::turn_off();
183 assert_eq!(off_command.mode, "StandBy");
184 }
185
186 #[test]
187 fn test_machine_command_json_serialization() {
188 let on_command = MachineCommand::turn_on();
189 let json = serde_json::to_string(&on_command).unwrap();
190 assert!(json.contains("BrewingMode"));
191
192 let off_command = MachineCommand::turn_off();
193 let json = serde_json::to_string(&off_command).unwrap();
194 assert!(json.contains("StandBy"));
195 }
196
197 #[test]
198 fn test_machine_status_parsing() {
199 let standby_status = MachineStatus {
201 widgets: vec![Widget {
202 code: "CMMachineStatus".to_string(),
203 output: Some(WidgetOutput {
204 status: Some("StandBy".to_string()),
205 mode: None,
206 ready_start_time: None,
207 }),
208 }],
209 };
210
211 assert!(!standby_status.is_on());
212 assert_eq!(standby_status.get_status_string(), "Standby");
213
214 let powered_on_status = MachineStatus {
216 widgets: vec![Widget {
217 code: "CMMachineStatus".to_string(),
218 output: Some(WidgetOutput {
219 status: Some("PoweredOn".to_string()),
220 mode: None,
221 ready_start_time: None,
222 }),
223 }],
224 };
225
226 assert!(powered_on_status.is_on());
227 assert_eq!(powered_on_status.get_status_string(), "On");
228
229 let no_status = MachineStatus { widgets: vec![] };
231
232 assert!(!no_status.is_on());
233 assert_eq!(no_status.get_status_string(), "Unknown");
234 }
235
236 #[test]
237 fn test_machine_status_with_ready_time() {
238 let status_warming = MachineStatus {
240 widgets: vec![
241 Widget {
242 code: "CMMachineStatus".to_string(),
243 output: Some(WidgetOutput {
244 status: Some("PoweredOn".to_string()),
245 mode: None,
246 ready_start_time: None, }),
248 },
249 Widget {
250 code: "CMCoffeeBoiler".to_string(),
251 output: Some(WidgetOutput {
252 status: Some("Heating".to_string()),
253 mode: None,
254 ready_start_time: Some(1748515947000), }),
256 },
257 ],
258 };
259
260 assert!(status_warming.is_on());
261
262 let fixed_current_time = 1748515647000; let warming_status = status_warming.get_status_string_with_time(Some(fixed_current_time));
267 assert_eq!(warming_status, "On (Ready in 5 mins)");
268
269 let warming_status_ready = status_warming.get_status_string_with_time(Some(1748515947000));
271 assert_eq!(warming_status_ready, "On (Ready in < 1 min)");
272
273 let warming_status_past = status_warming.get_status_string_with_time(Some(1748515947001));
275 assert_eq!(warming_status_past, "On (Ready in < 1 min)");
276
277 let one_minute_before = 1748515947000 - 60000; let warming_status_1min =
280 status_warming.get_status_string_with_time(Some(one_minute_before));
281 assert_eq!(warming_status_1min, "On (Ready in 1 min)");
282
283 let thirty_seconds_before = 1748515947000 - 30000; let warming_status_soon =
286 status_warming.get_status_string_with_time(Some(thirty_seconds_before));
287 assert_eq!(warming_status_soon, "On (Ready in < 1 min)");
288 }
289
290 #[test]
291 fn test_machine_status_error_conditions() {
292 let status = MachineStatus { widgets: vec![] };
294 assert!(!status.is_on());
295 assert_eq!(status.get_status_string(), "Unknown");
296
297 let status = MachineStatus {
299 widgets: vec![Widget {
300 code: "WrongWidget".to_string(),
301 output: Some(WidgetOutput {
302 status: Some("PoweredOn".to_string()),
303 mode: None,
304 ready_start_time: None,
305 }),
306 }],
307 };
308 assert!(!status.is_on());
309 assert_eq!(status.get_status_string(), "Unknown");
310
311 let status = MachineStatus {
313 widgets: vec![Widget {
314 code: "CMMachineStatus".to_string(),
315 output: None,
316 }],
317 };
318 assert!(!status.is_on());
319 assert_eq!(status.get_status_string(), "Unknown");
320
321 let status = MachineStatus {
323 widgets: vec![Widget {
324 code: "CMMachineStatus".to_string(),
325 output: Some(WidgetOutput {
326 status: None,
327 mode: Some("SomeMode".to_string()),
328 ready_start_time: None,
329 }),
330 }],
331 };
332 assert!(!status.is_on());
333 assert_eq!(status.get_status_string(), "Unknown");
334 }
335
336 #[test]
337 fn test_machine_status_no_water() {
338 let status_no_water = MachineStatus {
340 widgets: vec![
341 Widget {
342 code: "CMMachineStatus".to_string(),
343 output: Some(WidgetOutput {
344 status: Some("PoweredOn".to_string()),
345 mode: None,
346 ready_start_time: None,
347 }),
348 },
349 Widget {
350 code: "CMCoffeeBoiler".to_string(),
351 output: Some(WidgetOutput {
352 status: Some("NoWater".to_string()),
353 mode: None,
354 ready_start_time: None, }),
356 },
357 ],
358 };
359
360 assert!(status_no_water.is_on());
361 assert_eq!(status_no_water.get_status_string(), "On (No water)");
362 }
363
364 #[test]
365 fn test_machine_properties() {
366 let machine = Machine {
367 serial_number: "TEST123".to_string(),
368 model: Some("Test Model".to_string()),
369 name: Some("Test Machine".to_string()),
370 location: Some("Test Location".to_string()),
371 connected: false,
372 };
373
374 assert_eq!(machine.serial_number, "TEST123");
376 assert_eq!(machine.model, Some("Test Model".to_string()));
377 assert_eq!(machine.name, Some("Test Machine".to_string()));
378 assert_eq!(machine.location, Some("Test Location".to_string()));
379 assert!(!machine.connected);
380 }
381}