1use serde::{Deserialize, Serialize};
2
3use super::gen1_responses::{Gen1Meter, Gen1Relay, Gen1StatusResponse};
4use super::gen2_responses::{Gen2InputStatus, Gen2SwitchStatus};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct SwitchStatus {
8 pub id: u8,
9 pub output: bool,
10 pub source: Option<String>,
11 pub power_watts: Option<f64>,
12 pub voltage: Option<f64>,
13 pub current: Option<f64>,
14 pub frequency: Option<f64>,
15 pub temperature_c: Option<f64>,
16 pub total_energy_wh: Option<f64>,
17 pub timer_active: bool,
18 pub timer_remaining: Option<f64>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct InputStatus {
23 pub id: u8,
24 pub state: bool,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct WifiStatus {
29 pub connected: bool,
30 pub ssid: Option<String>,
31 pub ip: Option<String>,
32 pub rssi: Option<i32>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct DeviceStatus {
37 pub switches: Vec<SwitchStatus>,
38 pub inputs: Vec<InputStatus>,
39 pub wifi: Option<WifiStatus>,
40 pub uptime: Option<u64>,
41 pub time: Option<String>,
42 pub cloud_connected: Option<bool>,
43 pub mqtt_connected: Option<bool>,
44 pub ram_free: Option<u64>,
45 pub temperature_c: Option<f64>,
46}
47
48impl SwitchStatus {
49 pub fn from_gen1_relay(id: u8, relay: &Gen1Relay, meter: Option<&Gen1Meter>) -> Self {
50 let timer_remaining = if relay.has_timer {
51 relay.timer_remaining
52 } else {
53 None
54 };
55
56 let (power_watts, total_energy_wh) = if let Some(m) = meter {
57 (m.power, m.total)
58 } else {
59 (None, None)
60 };
61
62 Self {
63 id,
64 output: relay.ison,
65 source: relay.source.clone(),
66 power_watts,
67 voltage: None,
68 current: None,
69 frequency: None,
70 temperature_c: None,
71 total_energy_wh,
72 timer_active: relay.has_timer,
73 timer_remaining,
74 }
75 }
76
77 pub fn from_gen2_switch(sw: &Gen2SwitchStatus) -> Self {
78 let temperature_c = sw.temperature.as_ref().and_then(|t| t.t_c);
79 let total_energy_wh = sw.aenergy.as_ref().and_then(|e| e.total);
80 let timer_active = sw.timer_started_at.is_some() && sw.timer_duration.is_some();
81
82 Self {
83 id: sw.id,
84 output: sw.output,
85 source: sw.source.clone(),
86 power_watts: sw.apower,
87 voltage: sw.voltage,
88 current: sw.current,
89 frequency: sw.frequency,
90 temperature_c,
91 total_energy_wh,
92 timer_active,
93 timer_remaining: sw.timer_duration,
94 }
95 }
96
97 pub fn from_gen1_relay_json(
99 id: u8,
100 relay: &serde_json::Value,
101 meter: Option<&serde_json::Value>,
102 ) -> Self {
103 let relay: Gen1Relay = serde_json::from_value(relay.clone()).unwrap_or(Gen1Relay {
104 ison: false,
105 source: None,
106 has_timer: false,
107 timer_remaining: None,
108 });
109 let meter: Option<Gen1Meter> = meter.and_then(|m| serde_json::from_value(m.clone()).ok());
110 Self::from_gen1_relay(id, &relay, meter.as_ref())
111 }
112
113 pub fn from_gen2_switch_json(sw: &serde_json::Value) -> Self {
115 let sw: Gen2SwitchStatus = serde_json::from_value(sw.clone()).unwrap_or(Gen2SwitchStatus {
116 id: 0,
117 output: false,
118 source: None,
119 apower: None,
120 voltage: None,
121 current: None,
122 frequency: None,
123 temperature: None,
124 aenergy: None,
125 timer_started_at: None,
126 timer_duration: None,
127 });
128 Self::from_gen2_switch(&sw)
129 }
130}
131
132impl DeviceStatus {
133 pub fn from_gen1(status: &serde_json::Value) -> Self {
134 let resp: Gen1StatusResponse =
135 serde_json::from_value(status.clone()).unwrap_or(Gen1StatusResponse {
136 relays: Vec::new(),
137 meters: Vec::new(),
138 inputs: Vec::new(),
139 wifi_sta: None,
140 uptime: None,
141 time: None,
142 cloud: None,
143 mqtt: None,
144 ram_free: None,
145 tmp: None,
146 temperature: None,
147 });
148
149 let switches: Vec<SwitchStatus> = resp
150 .relays
151 .iter()
152 .enumerate()
153 .map(|(i, relay)| {
154 let meter = resp.meters.get(i);
155 SwitchStatus::from_gen1_relay(i as u8, relay, meter)
156 })
157 .collect();
158
159 let inputs: Vec<InputStatus> = resp
160 .inputs
161 .iter()
162 .enumerate()
163 .map(|(i, input)| InputStatus {
164 id: i as u8,
165 state: input.input != 0,
166 })
167 .collect();
168
169 let wifi = resp.wifi_sta.map(|w| WifiStatus {
170 connected: w.connected,
171 ssid: w.ssid,
172 ip: w.ip,
173 rssi: w.rssi.map(|v| v as i32),
174 });
175
176 let temperature_c = resp.tmp.and_then(|t| t.t_c).or(resp.temperature);
177
178 Self {
179 switches,
180 inputs,
181 wifi,
182 uptime: resp.uptime,
183 time: resp.time,
184 cloud_connected: resp.cloud.and_then(|c| c.connected),
185 mqtt_connected: resp.mqtt.and_then(|m| m.connected),
186 ram_free: resp.ram_free,
187 temperature_c,
188 }
189 }
190
191 pub fn from_gen2(status: &serde_json::Value) -> Self {
192 let mut switches = Vec::new();
193 let mut inputs = Vec::new();
194
195 for (key, value) in status.as_object().into_iter().flatten() {
197 if key.starts_with("switch:") {
198 if let Ok(sw) = serde_json::from_value::<Gen2SwitchStatus>(value.clone()) {
199 switches.push(SwitchStatus::from_gen2_switch(&sw));
200 }
201 } else if key.starts_with("input:")
202 && let Ok(input) = serde_json::from_value::<Gen2InputStatus>(value.clone())
203 {
204 inputs.push(InputStatus {
205 id: input.id,
206 state: input.state,
207 });
208 }
209 }
210
211 switches.sort_by_key(|s| s.id);
212 inputs.sort_by_key(|i| i.id);
213
214 let wifi = status
215 .get("wifi")
216 .and_then(|w| {
217 serde_json::from_value::<super::gen2_responses::Gen2WifiStatus>(w.clone()).ok()
218 })
219 .map(|w| WifiStatus {
220 connected: w.status.as_deref() == Some("got ip"),
221 ssid: w.ssid,
222 ip: w.sta_ip,
223 rssi: w.rssi.map(|v| v as i32),
224 });
225
226 let sys = status.get("sys").and_then(|s| {
227 serde_json::from_value::<super::gen2_responses::Gen2SysStatus>(s.clone()).ok()
228 });
229
230 let cloud_connected = status
231 .get("cloud")
232 .and_then(|c| {
233 serde_json::from_value::<super::gen2_responses::Gen2Cloud>(c.clone()).ok()
234 })
235 .and_then(|c| c.connected);
236
237 let mqtt_connected = status
238 .get("mqtt")
239 .and_then(|m| serde_json::from_value::<super::gen2_responses::Gen2Mqtt>(m.clone()).ok())
240 .and_then(|m| m.connected);
241
242 let temperature_c = switches.first().and_then(|s| s.temperature_c);
243
244 Self {
245 switches,
246 inputs,
247 wifi,
248 uptime: sys.as_ref().and_then(|s| s.uptime),
249 time: sys.as_ref().and_then(|s| s.time.clone()),
250 cloud_connected,
251 mqtt_connected,
252 ram_free: sys.and_then(|s| s.ram_free),
253 temperature_c,
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use serde_json::json;
262
263 #[test]
264 fn gen1_full_status() {
265 let status = json!({
266 "relays": [
267 {"ison": true, "source": "http", "has_timer": false, "timer_remaining": 0}
268 ],
269 "meters": [
270 {"power": 42.5, "total": 12345.6}
271 ],
272 "inputs": [
273 {"input": 1}
274 ],
275 "wifi_sta": {
276 "connected": true,
277 "ssid": "MyNetwork",
278 "ip": "192.0.2.5",
279 "rssi": -58
280 },
281 "uptime": 86400,
282 "time": "14:30",
283 "cloud": {"connected": true},
284 "mqtt": {"connected": false},
285 "ram_free": 32000,
286 "tmp": {"tC": 38.5, "tF": 101.3, "is_valid": true}
287 });
288
289 let ds = DeviceStatus::from_gen1(&status);
290 assert_eq!(ds.switches.len(), 1);
291 assert!(ds.switches[0].output);
292 assert_eq!(ds.switches[0].source.as_deref(), Some("http"));
293 assert_eq!(ds.switches[0].power_watts, Some(42.5));
294 assert_eq!(ds.switches[0].total_energy_wh, Some(12345.6));
295 assert!(!ds.switches[0].timer_active);
296
297 assert_eq!(ds.inputs.len(), 1);
298 assert!(ds.inputs[0].state);
299
300 let wifi = ds.wifi.unwrap();
301 assert!(wifi.connected);
302 assert_eq!(wifi.ssid.as_deref(), Some("MyNetwork"));
303 assert_eq!(wifi.ip.as_deref(), Some("192.0.2.5"));
304 assert_eq!(wifi.rssi, Some(-58));
305
306 assert_eq!(ds.uptime, Some(86400));
307 assert_eq!(ds.time.as_deref(), Some("14:30"));
308 assert_eq!(ds.cloud_connected, Some(true));
309 assert_eq!(ds.mqtt_connected, Some(false));
310 assert_eq!(ds.ram_free, Some(32000));
311 assert_eq!(ds.temperature_c, Some(38.5));
312 }
313
314 #[test]
315 fn gen2_full_status() {
316 let status = json!({
317 "switch:0": {
318 "id": 0,
319 "source": "WS",
320 "output": true,
321 "apower": 100.5,
322 "voltage": 224.0,
323 "current": 0.45,
324 "freq": 50.0,
325 "temperature": {"tC": 42.0, "tF": 107.6},
326 "aenergy": {"total": 5678.9, "by_minute": [0.0], "minute_ts": 0}
327 },
328 "input:0": {
329 "id": 0,
330 "state": true
331 },
332 "wifi": {
333 "sta_ip": "192.0.2.10",
334 "status": "got ip",
335 "ssid": "HomeNet",
336 "rssi": -45
337 },
338 "sys": {
339 "uptime": 3600,
340 "time": "09:15",
341 "ram_free": 64000
342 },
343 "cloud": {"connected": true},
344 "mqtt": {"connected": true}
345 });
346
347 let ds = DeviceStatus::from_gen2(&status);
348 assert_eq!(ds.switches.len(), 1);
349 assert!(ds.switches[0].output);
350 assert_eq!(ds.switches[0].source.as_deref(), Some("WS"));
351 assert_eq!(ds.switches[0].power_watts, Some(100.5));
352 assert_eq!(ds.switches[0].voltage, Some(224.0));
353 assert_eq!(ds.switches[0].current, Some(0.45));
354 assert_eq!(ds.switches[0].frequency, Some(50.0));
355 assert_eq!(ds.switches[0].temperature_c, Some(42.0));
356 assert_eq!(ds.switches[0].total_energy_wh, Some(5678.9));
357
358 assert_eq!(ds.inputs.len(), 1);
359 assert!(ds.inputs[0].state);
360
361 let wifi = ds.wifi.unwrap();
362 assert!(wifi.connected);
363 assert_eq!(wifi.ssid.as_deref(), Some("HomeNet"));
364 assert_eq!(wifi.ip.as_deref(), Some("192.0.2.10"));
365 assert_eq!(wifi.rssi, Some(-45));
366
367 assert_eq!(ds.uptime, Some(3600));
368 assert_eq!(ds.time.as_deref(), Some("09:15"));
369 assert_eq!(ds.cloud_connected, Some(true));
370 assert_eq!(ds.mqtt_connected, Some(true));
371 assert_eq!(ds.ram_free, Some(64000));
372 assert_eq!(ds.temperature_c, Some(42.0));
373 }
374
375 #[test]
376 fn gen1_minimal_status() {
377 let status = json!({});
378
379 let ds = DeviceStatus::from_gen1(&status);
380 assert!(ds.switches.is_empty());
381 assert!(ds.inputs.is_empty());
382 assert!(ds.wifi.is_none());
383 assert!(ds.uptime.is_none());
384 assert!(ds.temperature_c.is_none());
385 }
386
387 #[test]
388 fn gen2_minimal_status() {
389 let status = json!({});
390
391 let ds = DeviceStatus::from_gen2(&status);
392 assert!(ds.switches.is_empty());
393 assert!(ds.inputs.is_empty());
394 assert!(ds.wifi.is_none());
395 assert!(ds.uptime.is_none());
396 }
397
398 #[test]
399 fn gen1_multiple_relays() {
400 let status = json!({
401 "relays": [
402 {"ison": true, "source": "http", "has_timer": false},
403 {"ison": false, "source": "switch", "has_timer": true, "timer_remaining": 30.0}
404 ],
405 "meters": [
406 {"power": 10.0, "total": 100.0},
407 {"power": 20.0, "total": 200.0}
408 ]
409 });
410
411 let ds = DeviceStatus::from_gen1(&status);
412 assert_eq!(ds.switches.len(), 2);
413
414 assert!(ds.switches[0].output);
415 assert_eq!(ds.switches[0].id, 0);
416 assert_eq!(ds.switches[0].power_watts, Some(10.0));
417
418 assert!(!ds.switches[1].output);
419 assert_eq!(ds.switches[1].id, 1);
420 assert_eq!(ds.switches[1].power_watts, Some(20.0));
421 assert!(ds.switches[1].timer_active);
422 assert_eq!(ds.switches[1].timer_remaining, Some(30.0));
423 }
424
425 #[test]
426 fn gen2_multiple_switches_sorted() {
427 let status = json!({
428 "switch:1": {"id": 1, "output": false},
429 "switch:0": {"id": 0, "output": true}
430 });
431
432 let ds = DeviceStatus::from_gen2(&status);
433 assert_eq!(ds.switches.len(), 2);
434 assert_eq!(ds.switches[0].id, 0);
435 assert!(ds.switches[0].output);
436 assert_eq!(ds.switches[1].id, 1);
437 assert!(!ds.switches[1].output);
438 }
439
440 #[test]
441 fn gen1_temperature_fallback() {
442 let status = json!({
444 "temperature": 35.0
445 });
446 let ds = DeviceStatus::from_gen1(&status);
447 assert_eq!(ds.temperature_c, Some(35.0));
448 }
449
450 #[test]
451 fn gen2_wifi_not_connected() {
452 let status = json!({
453 "wifi": {
454 "sta_ip": null,
455 "status": "disconnected",
456 "ssid": null,
457 "rssi": -90
458 }
459 });
460 let ds = DeviceStatus::from_gen2(&status);
461 let wifi = ds.wifi.unwrap();
462 assert!(!wifi.connected);
463 }
464
465 #[test]
466 fn gen2_timer_active() {
467 let status = json!({
468 "switch:0": {
469 "id": 0,
470 "output": true,
471 "timer_started_at": 1000.0,
472 "timer_duration": 60.0
473 }
474 });
475
476 let ds = DeviceStatus::from_gen2(&status);
477 assert!(ds.switches[0].timer_active);
478 assert_eq!(ds.switches[0].timer_remaining, Some(60.0));
479 }
480}