1use std::sync::atomic::{AtomicBool, AtomicI16, AtomicU32, AtomicU64, Ordering};
16use std::time::Duration;
17
18use async_trait::async_trait;
19use tokio::sync::RwLock;
20
21use aranet_types::{CurrentReading, DeviceInfo, DeviceType, HistoryRecord, Status};
22
23use crate::error::{Error, Result};
24use crate::history::{HistoryInfo, HistoryOptions};
25use crate::settings::{CalibrationData, MeasurementInterval};
26use crate::traits::AranetDevice;
27
28pub struct MockDevice {
51 name: String,
52 address: String,
53 device_type: DeviceType,
54 connected: AtomicBool,
55 current_reading: RwLock<CurrentReading>,
56 device_info: RwLock<DeviceInfo>,
57 history: RwLock<Vec<HistoryRecord>>,
58 interval: RwLock<MeasurementInterval>,
59 calibration: RwLock<CalibrationData>,
60 battery: RwLock<u8>,
61 rssi: AtomicI16,
62 read_count: AtomicU32,
63 should_fail: AtomicBool,
64 fail_message: RwLock<String>,
65 read_latency_ms: AtomicU64,
67 connect_latency_ms: AtomicU64,
69 fail_count: AtomicU32,
71 remaining_failures: AtomicU32,
73}
74
75impl std::fmt::Debug for MockDevice {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("MockDevice")
78 .field("name", &self.name)
79 .field("address", &self.address)
80 .field("device_type", &self.device_type)
81 .field("connected", &self.connected.load(Ordering::Relaxed))
82 .finish()
83 }
84}
85
86impl MockDevice {
87 pub fn new(name: &str, device_type: DeviceType) -> Self {
89 Self {
90 name: name.to_string(),
91 address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
92 device_type,
93 connected: AtomicBool::new(false),
94 current_reading: RwLock::new(Self::default_reading()),
95 device_info: RwLock::new(Self::default_info(name)),
96 history: RwLock::new(Vec::new()),
97 interval: RwLock::new(MeasurementInterval::FiveMinutes),
98 calibration: RwLock::new(CalibrationData::default()),
99 battery: RwLock::new(85),
100 rssi: AtomicI16::new(-50),
101 read_count: AtomicU32::new(0),
102 should_fail: AtomicBool::new(false),
103 fail_message: RwLock::new("Mock failure".to_string()),
104 read_latency_ms: AtomicU64::new(0),
105 connect_latency_ms: AtomicU64::new(0),
106 fail_count: AtomicU32::new(0),
107 remaining_failures: AtomicU32::new(0),
108 }
109 }
110
111 fn default_reading() -> CurrentReading {
112 CurrentReading {
113 co2: 800,
114 temperature: 22.5,
115 pressure: 1013.2,
116 humidity: 50,
117 battery: 85,
118 status: Status::Green,
119 interval: 300,
120 age: 60,
121 captured_at: None,
122 radon: None,
123 radiation_rate: None,
124 radiation_total: None,
125 radon_avg_24h: None,
126 radon_avg_7d: None,
127 radon_avg_30d: None,
128 }
129 }
130
131 fn default_info(name: &str) -> DeviceInfo {
132 DeviceInfo {
133 name: name.to_string(),
134 model: "Aranet4".to_string(),
135 serial: "MOCK-12345".to_string(),
136 firmware: "v1.5.0".to_string(),
137 hardware: "1.0".to_string(),
138 software: "1.5.0".to_string(),
139 manufacturer: "SAF Tehnika".to_string(),
140 }
141 }
142
143 pub async fn connect(&self) -> Result<()> {
145 use crate::error::DeviceNotFoundReason;
146
147 let latency = self.connect_latency_ms.load(Ordering::Relaxed);
149 if latency > 0 {
150 tokio::time::sleep(Duration::from_millis(latency)).await;
151 }
152
153 if self.remaining_failures.load(Ordering::Relaxed) > 0 {
155 self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
156 return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
157 identifier: self.name.clone(),
158 }));
159 }
160
161 if self.should_fail.load(Ordering::Relaxed) {
162 return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
163 identifier: self.name.clone(),
164 }));
165 }
166 self.connected.store(true, Ordering::Relaxed);
167 Ok(())
168 }
169
170 pub async fn disconnect(&self) -> Result<()> {
172 self.connected.store(false, Ordering::Relaxed);
173 Ok(())
174 }
175
176 pub fn is_connected_sync(&self) -> bool {
178 self.connected.load(Ordering::Relaxed)
179 }
180
181 pub fn name(&self) -> &str {
183 &self.name
184 }
185
186 pub fn address(&self) -> &str {
188 &self.address
189 }
190
191 pub fn device_type(&self) -> DeviceType {
193 self.device_type
194 }
195
196 pub async fn read_current(&self) -> Result<CurrentReading> {
198 self.check_connected()?;
199 self.check_should_fail().await?;
200
201 self.read_count.fetch_add(1, Ordering::Relaxed);
202 Ok(*self.current_reading.read().await)
203 }
204
205 pub async fn read_battery(&self) -> Result<u8> {
207 self.check_connected()?;
208 self.check_should_fail().await?;
209 Ok(*self.battery.read().await)
210 }
211
212 pub async fn read_rssi(&self) -> Result<i16> {
214 self.check_connected()?;
215 self.check_should_fail().await?;
216 Ok(self.rssi.load(Ordering::Relaxed))
217 }
218
219 pub async fn read_device_info(&self) -> Result<DeviceInfo> {
221 self.check_connected()?;
222 self.check_should_fail().await?;
223 Ok(self.device_info.read().await.clone())
224 }
225
226 pub async fn get_history_info(&self) -> Result<HistoryInfo> {
228 self.check_connected()?;
229 self.check_should_fail().await?;
230
231 let history = self.history.read().await;
232 let interval = self.interval.read().await;
233
234 Ok(HistoryInfo {
235 total_readings: history.len() as u16,
236 interval_seconds: interval.as_seconds(),
237 seconds_since_update: 60,
238 })
239 }
240
241 pub async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
243 self.check_connected()?;
244 self.check_should_fail().await?;
245 Ok(self.history.read().await.clone())
246 }
247
248 pub async fn download_history_with_options(
250 &self,
251 options: HistoryOptions,
252 ) -> Result<Vec<HistoryRecord>> {
253 self.check_connected()?;
254 self.check_should_fail().await?;
255
256 let history = self.history.read().await;
257 let start = options.start_index.unwrap_or(0) as usize;
258 let end = options
259 .end_index
260 .map(|e| e as usize)
261 .unwrap_or(history.len());
262
263 if let Some(ref _callback) = options.progress_callback {
265 let progress = crate::history::HistoryProgress::new(
267 crate::history::HistoryParam::Co2,
268 1,
269 1,
270 history.len().min(end).saturating_sub(start),
271 );
272 options.report_progress(&progress);
273 }
274
275 Ok(history
276 .iter()
277 .skip(start)
278 .take(end.saturating_sub(start))
279 .cloned()
280 .collect())
281 }
282
283 pub async fn get_interval(&self) -> Result<MeasurementInterval> {
285 self.check_connected()?;
286 self.check_should_fail().await?;
287 Ok(*self.interval.read().await)
288 }
289
290 pub async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
292 self.check_connected()?;
293 self.check_should_fail().await?;
294 *self.interval.write().await = interval;
295 Ok(())
296 }
297
298 pub async fn get_calibration(&self) -> Result<CalibrationData> {
300 self.check_connected()?;
301 self.check_should_fail().await?;
302 Ok(self.calibration.read().await.clone())
303 }
304
305 fn check_connected(&self) -> Result<()> {
306 if !self.connected.load(Ordering::Relaxed) {
307 Err(Error::NotConnected)
308 } else {
309 Ok(())
310 }
311 }
312
313 async fn check_should_fail(&self) -> Result<()> {
314 let latency = self.read_latency_ms.load(Ordering::Relaxed);
316 if latency > 0 {
317 tokio::time::sleep(Duration::from_millis(latency)).await;
318 }
319
320 if self.remaining_failures.load(Ordering::Relaxed) > 0 {
322 self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
323 return Err(Error::InvalidData(self.fail_message.read().await.clone()));
324 }
325
326 if self.should_fail.load(Ordering::Relaxed) {
327 Err(Error::InvalidData(self.fail_message.read().await.clone()))
328 } else {
329 Ok(())
330 }
331 }
332
333 pub async fn set_reading(&self, reading: CurrentReading) {
337 *self.current_reading.write().await = reading;
338 }
339
340 pub async fn set_co2(&self, co2: u16) {
342 self.current_reading.write().await.co2 = co2;
343 }
344
345 pub async fn set_temperature(&self, temp: f32) {
347 self.current_reading.write().await.temperature = temp;
348 }
349
350 pub async fn set_battery(&self, level: u8) {
352 *self.battery.write().await = level;
353 self.current_reading.write().await.battery = level;
354 }
355
356 pub fn set_rssi(&self, rssi: i16) {
358 self.rssi.store(rssi, Ordering::Relaxed);
359 }
360
361 pub async fn add_history(&self, records: Vec<HistoryRecord>) {
363 self.history.write().await.extend(records);
364 }
365
366 pub async fn set_should_fail(&self, fail: bool, message: Option<&str>) {
368 self.should_fail.store(fail, Ordering::Relaxed);
369 if let Some(msg) = message {
370 *self.fail_message.write().await = msg.to_string();
371 }
372 }
373
374 pub fn read_count(&self) -> u32 {
376 self.read_count.load(Ordering::Relaxed)
377 }
378
379 pub fn reset_read_count(&self) {
381 self.read_count.store(0, Ordering::Relaxed);
382 }
383
384 pub fn set_read_latency(&self, latency: Duration) {
389 self.read_latency_ms
390 .store(latency.as_millis() as u64, Ordering::Relaxed);
391 }
392
393 pub fn set_connect_latency(&self, latency: Duration) {
398 self.connect_latency_ms
399 .store(latency.as_millis() as u64, Ordering::Relaxed);
400 }
401
402 pub fn set_transient_failures(&self, count: u32) {
418 self.fail_count.store(count, Ordering::Relaxed);
419 self.remaining_failures.store(count, Ordering::Relaxed);
420 }
421
422 pub fn reset_transient_failures(&self) {
424 self.remaining_failures
425 .store(self.fail_count.load(Ordering::Relaxed), Ordering::Relaxed);
426 }
427
428 pub fn remaining_failures(&self) -> u32 {
430 self.remaining_failures.load(Ordering::Relaxed)
431 }
432}
433
434#[async_trait]
436impl AranetDevice for MockDevice {
437 async fn is_connected(&self) -> bool {
440 self.is_connected_sync()
441 }
442
443 async fn disconnect(&self) -> Result<()> {
444 MockDevice::disconnect(self).await
445 }
446
447 fn name(&self) -> Option<&str> {
450 Some(MockDevice::name(self))
451 }
452
453 fn address(&self) -> &str {
454 MockDevice::address(self)
455 }
456
457 fn device_type(&self) -> Option<DeviceType> {
458 Some(MockDevice::device_type(self))
459 }
460
461 async fn read_current(&self) -> Result<CurrentReading> {
464 MockDevice::read_current(self).await
465 }
466
467 async fn read_device_info(&self) -> Result<DeviceInfo> {
468 MockDevice::read_device_info(self).await
469 }
470
471 async fn read_rssi(&self) -> Result<i16> {
472 MockDevice::read_rssi(self).await
473 }
474
475 async fn read_battery(&self) -> Result<u8> {
478 MockDevice::read_battery(self).await
479 }
480
481 async fn get_history_info(&self) -> Result<crate::history::HistoryInfo> {
484 MockDevice::get_history_info(self).await
485 }
486
487 async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
488 MockDevice::download_history(self).await
489 }
490
491 async fn download_history_with_options(
492 &self,
493 options: HistoryOptions,
494 ) -> Result<Vec<HistoryRecord>> {
495 MockDevice::download_history_with_options(self, options).await
496 }
497
498 async fn get_interval(&self) -> Result<MeasurementInterval> {
501 MockDevice::get_interval(self).await
502 }
503
504 async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
505 MockDevice::set_interval(self, interval).await
506 }
507
508 async fn get_calibration(&self) -> Result<CalibrationData> {
509 MockDevice::get_calibration(self).await
510 }
511}
512
513#[derive(Debug)]
515pub struct MockDeviceBuilder {
516 name: String,
517 device_type: DeviceType,
518 co2: u16,
519 temperature: f32,
520 pressure: f32,
521 humidity: u8,
522 battery: u8,
523 status: Status,
524 auto_connect: bool,
525}
526
527impl Default for MockDeviceBuilder {
528 fn default() -> Self {
529 Self {
530 name: "Mock Aranet4".to_string(),
531 device_type: DeviceType::Aranet4,
532 co2: 800,
533 temperature: 22.5,
534 pressure: 1013.2,
535 humidity: 50,
536 battery: 85,
537 status: Status::Green,
538 auto_connect: true,
539 }
540 }
541}
542
543impl MockDeviceBuilder {
544 #[must_use]
546 pub fn new() -> Self {
547 Self::default()
548 }
549
550 #[must_use]
552 pub fn name(mut self, name: &str) -> Self {
553 self.name = name.to_string();
554 self
555 }
556
557 #[must_use]
559 pub fn device_type(mut self, device_type: DeviceType) -> Self {
560 self.device_type = device_type;
561 self
562 }
563
564 #[must_use]
566 pub fn co2(mut self, co2: u16) -> Self {
567 self.co2 = co2;
568 self
569 }
570
571 #[must_use]
573 pub fn temperature(mut self, temp: f32) -> Self {
574 self.temperature = temp;
575 self
576 }
577
578 #[must_use]
580 pub fn pressure(mut self, pressure: f32) -> Self {
581 self.pressure = pressure;
582 self
583 }
584
585 #[must_use]
587 pub fn humidity(mut self, humidity: u8) -> Self {
588 self.humidity = humidity;
589 self
590 }
591
592 #[must_use]
594 pub fn battery(mut self, battery: u8) -> Self {
595 self.battery = battery;
596 self
597 }
598
599 #[must_use]
601 pub fn status(mut self, status: Status) -> Self {
602 self.status = status;
603 self
604 }
605
606 #[must_use]
608 pub fn auto_connect(mut self, auto: bool) -> Self {
609 self.auto_connect = auto;
610 self
611 }
612
613 #[must_use]
618 pub fn build(self) -> MockDevice {
619 let reading = CurrentReading {
620 co2: self.co2,
621 temperature: self.temperature,
622 pressure: self.pressure,
623 humidity: self.humidity,
624 battery: self.battery,
625 status: self.status,
626 interval: 300,
627 age: 60,
628 captured_at: None,
629 radon: None,
630 radiation_rate: None,
631 radiation_total: None,
632 radon_avg_24h: None,
633 radon_avg_7d: None,
634 radon_avg_30d: None,
635 };
636
637 MockDevice {
638 name: self.name.clone(),
639 address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
640 device_type: self.device_type,
641 connected: AtomicBool::new(self.auto_connect),
642 current_reading: RwLock::new(reading),
643 device_info: RwLock::new(MockDevice::default_info(&self.name)),
644 history: RwLock::new(Vec::new()),
645 interval: RwLock::new(MeasurementInterval::FiveMinutes),
646 calibration: RwLock::new(CalibrationData::default()),
647 battery: RwLock::new(self.battery),
648 rssi: AtomicI16::new(-50),
649 read_count: AtomicU32::new(0),
650 should_fail: AtomicBool::new(false),
651 fail_message: RwLock::new("Mock failure".to_string()),
652 read_latency_ms: AtomicU64::new(0),
653 connect_latency_ms: AtomicU64::new(0),
654 fail_count: AtomicU32::new(0),
655 remaining_failures: AtomicU32::new(0),
656 }
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use crate::traits::AranetDevice;
664
665 #[tokio::test]
666 async fn test_mock_device_connect() {
667 let device = MockDevice::new("Test", DeviceType::Aranet4);
668 assert!(!device.is_connected_sync());
669
670 device.connect().await.unwrap();
671 assert!(device.is_connected_sync());
672
673 device.disconnect().await.unwrap();
674 assert!(!device.is_connected_sync());
675 }
676
677 #[tokio::test]
678 async fn test_mock_device_read() {
679 let device = MockDeviceBuilder::new().co2(1200).temperature(25.0).build();
680
681 let reading = device.read_current().await.unwrap();
682 assert_eq!(reading.co2, 1200);
683 assert!((reading.temperature - 25.0).abs() < 0.01);
684 }
685
686 #[tokio::test]
687 async fn test_mock_device_fail() {
688 let device = MockDeviceBuilder::new().build();
689 device.set_should_fail(true, Some("Test error")).await;
690
691 let result = device.read_current().await;
692 assert!(result.is_err());
693 assert!(result.unwrap_err().to_string().contains("Test error"));
694 }
695
696 #[tokio::test]
697 async fn test_mock_device_not_connected() {
698 let device = MockDeviceBuilder::new().auto_connect(false).build();
699
700 let result = device.read_current().await;
701 assert!(matches!(result, Err(Error::NotConnected)));
702 }
703
704 #[test]
705 fn test_builder_defaults() {
706 let device = MockDeviceBuilder::new().build();
707 assert!(device.is_connected_sync());
708 assert_eq!(device.device_type(), DeviceType::Aranet4);
709 }
710
711 #[tokio::test]
712 async fn test_aranet_device_trait() {
713 let device = MockDeviceBuilder::new().co2(1000).build();
714
715 async fn check_via_trait<D: AranetDevice>(d: &D) -> u16 {
717 d.read_current().await.unwrap().co2
718 }
719
720 assert_eq!(check_via_trait(&device).await, 1000);
721 }
722}