Skip to main content

aranet_core/
events.rs

1//! Device event system for connection and reading notifications.
2//!
3//! This module provides an event-based system for receiving notifications
4//! about device connections, disconnections, readings, and errors.
5
6use serde::{Deserialize, Serialize};
7use tokio::sync::broadcast;
8
9use aranet_types::{CurrentReading, DeviceInfo, DeviceType};
10
11/// Device identifier for events.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct DeviceId {
14    /// Unique identifier (peripheral ID or MAC address).
15    pub id: String,
16    /// Device name if known.
17    pub name: Option<String>,
18    /// Device type if known.
19    pub device_type: Option<DeviceType>,
20}
21
22impl DeviceId {
23    /// Create a new device ID.
24    pub fn new(id: impl Into<String>) -> Self {
25        Self {
26            id: id.into(),
27            name: None,
28            device_type: None,
29        }
30    }
31
32    /// Create a device ID with name.
33    pub fn with_name(id: impl Into<String>, name: impl Into<String>) -> Self {
34        Self {
35            id: id.into(),
36            name: Some(name.into()),
37            device_type: None,
38        }
39    }
40}
41
42/// Events that can be emitted by devices.
43///
44/// All events are serializable for logging, persistence, and IPC.
45///
46/// This enum is marked `#[non_exhaustive]` to allow adding new event types
47/// in future versions without breaking downstream code.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(tag = "type", rename_all = "snake_case")]
50#[non_exhaustive]
51pub enum DeviceEvent {
52    /// Device was discovered during scanning.
53    Discovered { device: DeviceId, rssi: Option<i16> },
54    /// Successfully connected to device.
55    Connected {
56        device: DeviceId,
57        info: Option<DeviceInfo>,
58    },
59    /// Disconnected from device.
60    Disconnected {
61        device: DeviceId,
62        reason: DisconnectReason,
63    },
64    /// New reading received from device.
65    Reading {
66        device: DeviceId,
67        reading: CurrentReading,
68    },
69    /// Error occurred during device operation.
70    Error { device: DeviceId, error: String },
71    /// Reconnection attempt started.
72    ReconnectStarted { device: DeviceId, attempt: u32 },
73    /// Reconnection succeeded.
74    ReconnectSucceeded { device: DeviceId, attempts: u32 },
75    /// Battery level changed significantly.
76    BatteryLow { device: DeviceId, level: u8 },
77}
78
79/// Reason for disconnection.
80///
81/// This enum is marked `#[non_exhaustive]` to allow adding new reasons
82/// in future versions without breaking downstream code.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[non_exhaustive]
85pub enum DisconnectReason {
86    /// Normal disconnection requested by user.
87    UserRequested,
88    /// Device went out of range.
89    OutOfRange,
90    /// Connection timed out.
91    Timeout,
92    /// Device was powered off.
93    DevicePoweredOff,
94    /// BLE error occurred.
95    BleError(String),
96    /// Unknown reason.
97    Unknown,
98}
99
100/// Sender for device events.
101pub type EventSender = broadcast::Sender<DeviceEvent>;
102
103/// Receiver for device events.
104pub type EventReceiver = broadcast::Receiver<DeviceEvent>;
105
106/// Create a new event channel with the given capacity.
107pub fn event_channel(capacity: usize) -> (EventSender, EventReceiver) {
108    broadcast::channel(capacity)
109}
110
111/// Create a default event channel with capacity 100.
112pub fn default_event_channel() -> (EventSender, EventReceiver) {
113    event_channel(100)
114}
115
116/// Event dispatcher for sending events to multiple receivers.
117#[derive(Debug, Clone)]
118pub struct EventDispatcher {
119    sender: EventSender,
120}
121
122impl EventDispatcher {
123    /// Create a new event dispatcher.
124    pub fn new(capacity: usize) -> Self {
125        let (sender, _) = broadcast::channel(capacity);
126        Self { sender }
127    }
128
129    /// Subscribe to events.
130    pub fn subscribe(&self) -> EventReceiver {
131        self.sender.subscribe()
132    }
133
134    /// Send an event.
135    pub fn send(&self, event: DeviceEvent) {
136        // Ignore error if no receivers
137        let _ = self.sender.send(event);
138    }
139
140    /// Get the number of active receivers.
141    pub fn receiver_count(&self) -> usize {
142        self.sender.receiver_count()
143    }
144
145    /// Get the sender for direct use.
146    pub fn sender(&self) -> EventSender {
147        self.sender.clone()
148    }
149}
150
151impl Default for EventDispatcher {
152    fn default() -> Self {
153        Self::new(100)
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use aranet_types::{CurrentReading, DeviceType, Status};
161
162    // ==================== DeviceId Tests ====================
163
164    #[test]
165    fn test_device_id_new() {
166        let id = DeviceId::new("AA:BB:CC:DD:EE:FF");
167        assert_eq!(id.id, "AA:BB:CC:DD:EE:FF");
168        assert!(id.name.is_none());
169        assert!(id.device_type.is_none());
170    }
171
172    #[test]
173    fn test_device_id_with_name() {
174        let id = DeviceId::with_name("AA:BB:CC:DD:EE:FF", "Kitchen Sensor");
175        assert_eq!(id.id, "AA:BB:CC:DD:EE:FF");
176        assert_eq!(id.name, Some("Kitchen Sensor".to_string()));
177        assert!(id.device_type.is_none());
178    }
179
180    #[test]
181    fn test_device_id_with_device_type() {
182        let mut id = DeviceId::new("test-id");
183        id.device_type = Some(DeviceType::Aranet4);
184        assert_eq!(id.device_type, Some(DeviceType::Aranet4));
185    }
186
187    #[test]
188    fn test_device_id_equality() {
189        let id1 = DeviceId::new("test");
190        let id2 = DeviceId::new("test");
191        let id3 = DeviceId::new("different");
192
193        assert_eq!(id1, id2);
194        assert_ne!(id1, id3);
195    }
196
197    #[test]
198    fn test_device_id_clone() {
199        let id1 = DeviceId::with_name("test", "name");
200        let id2 = id1.clone();
201        assert_eq!(id1, id2);
202    }
203
204    #[test]
205    fn test_device_id_serialization() {
206        let id = DeviceId::with_name("device-123", "My Device");
207        let json = serde_json::to_string(&id).unwrap();
208        assert!(json.contains("device-123"));
209        assert!(json.contains("My Device"));
210
211        let deserialized: DeviceId = serde_json::from_str(&json).unwrap();
212        assert_eq!(deserialized, id);
213    }
214
215    // ==================== DisconnectReason Tests ====================
216
217    #[test]
218    fn test_disconnect_reason_equality() {
219        assert_eq!(
220            DisconnectReason::UserRequested,
221            DisconnectReason::UserRequested
222        );
223        assert_eq!(DisconnectReason::OutOfRange, DisconnectReason::OutOfRange);
224        assert_eq!(DisconnectReason::Timeout, DisconnectReason::Timeout);
225        assert_eq!(
226            DisconnectReason::DevicePoweredOff,
227            DisconnectReason::DevicePoweredOff
228        );
229        assert_eq!(DisconnectReason::Unknown, DisconnectReason::Unknown);
230
231        assert_ne!(DisconnectReason::UserRequested, DisconnectReason::Timeout);
232    }
233
234    #[test]
235    fn test_disconnect_reason_ble_error() {
236        let reason1 = DisconnectReason::BleError("error 1".to_string());
237        let reason2 = DisconnectReason::BleError("error 1".to_string());
238        let reason3 = DisconnectReason::BleError("error 2".to_string());
239
240        assert_eq!(reason1, reason2);
241        assert_ne!(reason1, reason3);
242    }
243
244    #[test]
245    fn test_disconnect_reason_serialization() {
246        for reason in [
247            DisconnectReason::UserRequested,
248            DisconnectReason::OutOfRange,
249            DisconnectReason::Timeout,
250            DisconnectReason::DevicePoweredOff,
251            DisconnectReason::BleError("test error".to_string()),
252            DisconnectReason::Unknown,
253        ] {
254            let json = serde_json::to_string(&reason).unwrap();
255            let deserialized: DisconnectReason = serde_json::from_str(&json).unwrap();
256            assert_eq!(deserialized, reason);
257        }
258    }
259
260    #[test]
261    fn test_disconnect_reason_clone() {
262        let reason = DisconnectReason::BleError("connection lost".to_string());
263        let cloned = reason.clone();
264        assert_eq!(reason, cloned);
265    }
266
267    // ==================== DeviceEvent Tests ====================
268
269    fn create_test_reading() -> CurrentReading {
270        CurrentReading {
271            co2: 800,
272            temperature: 22.5,
273            pressure: 1013.0,
274            humidity: 45,
275            battery: 85,
276            status: Status::Green,
277            interval: 60,
278            age: 30,
279            captured_at: None,
280            radon: None,
281            radiation_rate: None,
282            radiation_total: None,
283            radon_avg_24h: None,
284            radon_avg_7d: None,
285            radon_avg_30d: None,
286        }
287    }
288
289    #[test]
290    fn test_device_event_discovered() {
291        let event = DeviceEvent::Discovered {
292            device: DeviceId::new("test"),
293            rssi: Some(-65),
294        };
295
296        let json = serde_json::to_string(&event).unwrap();
297        assert!(json.contains("discovered"));
298        assert!(json.contains("-65"));
299    }
300
301    #[test]
302    fn test_device_event_connected() {
303        let event = DeviceEvent::Connected {
304            device: DeviceId::new("test"),
305            info: None,
306        };
307
308        let json = serde_json::to_string(&event).unwrap();
309        assert!(json.contains("connected"));
310    }
311
312    #[test]
313    fn test_device_event_disconnected() {
314        let event = DeviceEvent::Disconnected {
315            device: DeviceId::new("test"),
316            reason: DisconnectReason::UserRequested,
317        };
318
319        let json = serde_json::to_string(&event).unwrap();
320        assert!(json.contains("disconnected"));
321    }
322
323    #[test]
324    fn test_device_event_reading() {
325        let event = DeviceEvent::Reading {
326            device: DeviceId::new("test"),
327            reading: create_test_reading(),
328        };
329
330        let json = serde_json::to_string(&event).unwrap();
331        assert!(json.contains("reading"));
332        assert!(json.contains("800")); // CO2 value
333    }
334
335    #[test]
336    fn test_device_event_error() {
337        let event = DeviceEvent::Error {
338            device: DeviceId::new("test"),
339            error: "Connection timeout".to_string(),
340        };
341
342        let json = serde_json::to_string(&event).unwrap();
343        assert!(json.contains("error"));
344        assert!(json.contains("Connection timeout"));
345    }
346
347    #[test]
348    fn test_device_event_reconnect_started() {
349        let event = DeviceEvent::ReconnectStarted {
350            device: DeviceId::new("test"),
351            attempt: 3,
352        };
353
354        let json = serde_json::to_string(&event).unwrap();
355        assert!(json.contains("reconnect_started"));
356        assert!(json.contains("3"));
357    }
358
359    #[test]
360    fn test_device_event_reconnect_succeeded() {
361        let event = DeviceEvent::ReconnectSucceeded {
362            device: DeviceId::new("test"),
363            attempts: 2,
364        };
365
366        let json = serde_json::to_string(&event).unwrap();
367        assert!(json.contains("reconnect_succeeded"));
368    }
369
370    #[test]
371    fn test_device_event_battery_low() {
372        let event = DeviceEvent::BatteryLow {
373            device: DeviceId::new("test"),
374            level: 10,
375        };
376
377        let json = serde_json::to_string(&event).unwrap();
378        assert!(json.contains("battery_low"));
379        assert!(json.contains("10"));
380    }
381
382    #[test]
383    fn test_device_event_clone() {
384        let event = DeviceEvent::Reading {
385            device: DeviceId::new("test"),
386            reading: create_test_reading(),
387        };
388
389        let cloned = event.clone();
390        match cloned {
391            DeviceEvent::Reading { device, reading } => {
392                assert_eq!(device.id, "test");
393                assert_eq!(reading.co2, 800);
394            }
395            _ => unreachable!("Clone should preserve event type as Reading"),
396        }
397    }
398
399    // ==================== Event Channel Tests ====================
400
401    #[test]
402    fn test_event_channel() {
403        let (tx, rx) = event_channel(50);
404        // Receiver is returned by channel(), so count starts at 1
405        assert_eq!(tx.receiver_count(), 1);
406        drop(rx);
407        assert_eq!(tx.receiver_count(), 0);
408    }
409
410    #[test]
411    fn test_default_event_channel() {
412        let (tx, rx) = default_event_channel();
413        // Receiver is returned by channel(), so count starts at 1
414        assert_eq!(tx.receiver_count(), 1);
415        drop(rx);
416        assert_eq!(tx.receiver_count(), 0);
417    }
418
419    #[tokio::test]
420    async fn test_event_channel_send_receive() {
421        let (tx, mut rx) = event_channel(10);
422
423        let event = DeviceEvent::Discovered {
424            device: DeviceId::new("test"),
425            rssi: Some(-70),
426        };
427
428        tx.send(event.clone()).unwrap();
429
430        let received = rx.recv().await.unwrap();
431        match received {
432            DeviceEvent::Discovered { device, rssi } => {
433                assert_eq!(device.id, "test");
434                assert_eq!(rssi, Some(-70));
435            }
436            _ => unreachable!("Expected Discovered event"),
437        }
438    }
439
440    // ==================== EventDispatcher Tests ====================
441
442    #[test]
443    fn test_event_dispatcher_new() {
444        let dispatcher = EventDispatcher::new(50);
445        assert_eq!(dispatcher.receiver_count(), 0);
446    }
447
448    #[test]
449    fn test_event_dispatcher_default() {
450        let dispatcher = EventDispatcher::default();
451        assert_eq!(dispatcher.receiver_count(), 0);
452    }
453
454    #[test]
455    fn test_event_dispatcher_subscribe() {
456        let dispatcher = EventDispatcher::new(10);
457        assert_eq!(dispatcher.receiver_count(), 0);
458
459        let _rx1 = dispatcher.subscribe();
460        assert_eq!(dispatcher.receiver_count(), 1);
461
462        let _rx2 = dispatcher.subscribe();
463        assert_eq!(dispatcher.receiver_count(), 2);
464    }
465
466    #[tokio::test]
467    async fn test_event_dispatcher_send_receive() {
468        let dispatcher = EventDispatcher::new(10);
469        let mut rx = dispatcher.subscribe();
470
471        let event = DeviceEvent::Connected {
472            device: DeviceId::with_name("test", "Test Device"),
473            info: None,
474        };
475
476        dispatcher.send(event);
477
478        let received = rx.recv().await.unwrap();
479        match received {
480            DeviceEvent::Connected { device, .. } => {
481                assert_eq!(device.id, "test");
482                assert_eq!(device.name, Some("Test Device".to_string()));
483            }
484            _ => unreachable!("Expected Connected event"),
485        }
486    }
487
488    #[tokio::test]
489    async fn test_event_dispatcher_multiple_receivers() {
490        let dispatcher = EventDispatcher::new(10);
491        let mut rx1 = dispatcher.subscribe();
492        let mut rx2 = dispatcher.subscribe();
493
494        let event = DeviceEvent::Discovered {
495            device: DeviceId::new("multi-test"),
496            rssi: Some(-50),
497        };
498
499        dispatcher.send(event);
500
501        // Both receivers should get the event
502        let received1 = rx1.recv().await.unwrap();
503        let received2 = rx2.recv().await.unwrap();
504
505        match (received1, received2) {
506            (
507                DeviceEvent::Discovered { device: d1, .. },
508                DeviceEvent::Discovered { device: d2, .. },
509            ) => {
510                assert_eq!(d1.id, "multi-test");
511                assert_eq!(d2.id, "multi-test");
512            }
513            _ => unreachable!("Expected Discovered events from both receivers"),
514        }
515    }
516
517    #[test]
518    fn test_event_dispatcher_send_no_receivers() {
519        let dispatcher = EventDispatcher::new(10);
520
521        // Should not panic even with no receivers
522        let event = DeviceEvent::Error {
523            device: DeviceId::new("test"),
524            error: "no one listening".to_string(),
525        };
526
527        dispatcher.send(event);
528    }
529
530    #[test]
531    fn test_event_dispatcher_sender() {
532        let dispatcher = EventDispatcher::new(10);
533        let sender = dispatcher.sender();
534
535        // Sender should work independently
536        assert_eq!(sender.receiver_count(), 0);
537    }
538
539    #[test]
540    fn test_event_dispatcher_clone() {
541        let dispatcher1 = EventDispatcher::new(10);
542        let _rx1 = dispatcher1.subscribe();
543
544        let dispatcher2 = dispatcher1.clone();
545
546        // Both dispatchers share the same channel
547        assert_eq!(dispatcher1.receiver_count(), 1);
548        assert_eq!(dispatcher2.receiver_count(), 1);
549
550        let _rx2 = dispatcher2.subscribe();
551        assert_eq!(dispatcher1.receiver_count(), 2);
552        assert_eq!(dispatcher2.receiver_count(), 2);
553    }
554
555    #[test]
556    fn test_event_dispatcher_debug() {
557        let dispatcher = EventDispatcher::new(10);
558        let debug = format!("{:?}", dispatcher);
559        assert!(debug.contains("EventDispatcher"));
560    }
561}