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 }
126 }
127
128 fn default_info(name: &str) -> DeviceInfo {
129 DeviceInfo {
130 name: name.to_string(),
131 model: "Aranet4".to_string(),
132 serial: "MOCK-12345".to_string(),
133 firmware: "v1.5.0".to_string(),
134 hardware: "1.0".to_string(),
135 software: "1.5.0".to_string(),
136 manufacturer: "SAF Tehnika".to_string(),
137 }
138 }
139
140 pub async fn connect(&self) -> Result<()> {
142 use crate::error::DeviceNotFoundReason;
143
144 let latency = self.connect_latency_ms.load(Ordering::Relaxed);
146 if latency > 0 {
147 tokio::time::sleep(Duration::from_millis(latency)).await;
148 }
149
150 if self.remaining_failures.load(Ordering::Relaxed) > 0 {
152 self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
153 return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
154 identifier: self.name.clone(),
155 }));
156 }
157
158 if self.should_fail.load(Ordering::Relaxed) {
159 return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
160 identifier: self.name.clone(),
161 }));
162 }
163 self.connected.store(true, Ordering::Relaxed);
164 Ok(())
165 }
166
167 pub async fn disconnect(&self) -> Result<()> {
169 self.connected.store(false, Ordering::Relaxed);
170 Ok(())
171 }
172
173 pub fn is_connected_sync(&self) -> bool {
175 self.connected.load(Ordering::Relaxed)
176 }
177
178 pub fn name(&self) -> &str {
180 &self.name
181 }
182
183 pub fn address(&self) -> &str {
185 &self.address
186 }
187
188 pub fn device_type(&self) -> DeviceType {
190 self.device_type
191 }
192
193 pub async fn read_current(&self) -> Result<CurrentReading> {
195 self.check_connected()?;
196 self.check_should_fail().await?;
197
198 self.read_count.fetch_add(1, Ordering::Relaxed);
199 Ok(*self.current_reading.read().await)
200 }
201
202 pub async fn read_battery(&self) -> Result<u8> {
204 self.check_connected()?;
205 self.check_should_fail().await?;
206 Ok(*self.battery.read().await)
207 }
208
209 pub async fn read_rssi(&self) -> Result<i16> {
211 self.check_connected()?;
212 self.check_should_fail().await?;
213 Ok(self.rssi.load(Ordering::Relaxed))
214 }
215
216 pub async fn read_device_info(&self) -> Result<DeviceInfo> {
218 self.check_connected()?;
219 self.check_should_fail().await?;
220 Ok(self.device_info.read().await.clone())
221 }
222
223 pub async fn get_history_info(&self) -> Result<HistoryInfo> {
225 self.check_connected()?;
226 self.check_should_fail().await?;
227
228 let history = self.history.read().await;
229 let interval = self.interval.read().await;
230
231 Ok(HistoryInfo {
232 total_readings: history.len() as u16,
233 interval_seconds: interval.as_seconds(),
234 seconds_since_update: 60,
235 })
236 }
237
238 pub async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
240 self.check_connected()?;
241 self.check_should_fail().await?;
242 Ok(self.history.read().await.clone())
243 }
244
245 pub async fn download_history_with_options(
247 &self,
248 options: HistoryOptions,
249 ) -> Result<Vec<HistoryRecord>> {
250 self.check_connected()?;
251 self.check_should_fail().await?;
252
253 let history = self.history.read().await;
254 let start = options.start_index.unwrap_or(0) as usize;
255 let end = options
256 .end_index
257 .map(|e| e as usize)
258 .unwrap_or(history.len());
259
260 if let Some(ref _callback) = options.progress_callback {
262 let progress = crate::history::HistoryProgress::new(
264 crate::history::HistoryParam::Co2,
265 1,
266 1,
267 history.len().min(end).saturating_sub(start),
268 );
269 options.report_progress(&progress);
270 }
271
272 Ok(history
273 .iter()
274 .skip(start)
275 .take(end.saturating_sub(start))
276 .cloned()
277 .collect())
278 }
279
280 pub async fn get_interval(&self) -> Result<MeasurementInterval> {
282 self.check_connected()?;
283 self.check_should_fail().await?;
284 Ok(*self.interval.read().await)
285 }
286
287 pub async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
289 self.check_connected()?;
290 self.check_should_fail().await?;
291 *self.interval.write().await = interval;
292 Ok(())
293 }
294
295 pub async fn get_calibration(&self) -> Result<CalibrationData> {
297 self.check_connected()?;
298 self.check_should_fail().await?;
299 Ok(self.calibration.read().await.clone())
300 }
301
302 fn check_connected(&self) -> Result<()> {
303 if !self.connected.load(Ordering::Relaxed) {
304 Err(Error::NotConnected)
305 } else {
306 Ok(())
307 }
308 }
309
310 async fn check_should_fail(&self) -> Result<()> {
311 let latency = self.read_latency_ms.load(Ordering::Relaxed);
313 if latency > 0 {
314 tokio::time::sleep(Duration::from_millis(latency)).await;
315 }
316
317 if self.remaining_failures.load(Ordering::Relaxed) > 0 {
319 self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
320 return Err(Error::InvalidData(self.fail_message.read().await.clone()));
321 }
322
323 if self.should_fail.load(Ordering::Relaxed) {
324 Err(Error::InvalidData(self.fail_message.read().await.clone()))
325 } else {
326 Ok(())
327 }
328 }
329
330 pub async fn set_reading(&self, reading: CurrentReading) {
334 *self.current_reading.write().await = reading;
335 }
336
337 pub async fn set_co2(&self, co2: u16) {
339 self.current_reading.write().await.co2 = co2;
340 }
341
342 pub async fn set_temperature(&self, temp: f32) {
344 self.current_reading.write().await.temperature = temp;
345 }
346
347 pub async fn set_battery(&self, level: u8) {
349 *self.battery.write().await = level;
350 self.current_reading.write().await.battery = level;
351 }
352
353 pub fn set_rssi(&self, rssi: i16) {
355 self.rssi.store(rssi, Ordering::Relaxed);
356 }
357
358 pub async fn add_history(&self, records: Vec<HistoryRecord>) {
360 self.history.write().await.extend(records);
361 }
362
363 pub async fn set_should_fail(&self, fail: bool, message: Option<&str>) {
365 self.should_fail.store(fail, Ordering::Relaxed);
366 if let Some(msg) = message {
367 *self.fail_message.write().await = msg.to_string();
368 }
369 }
370
371 pub fn read_count(&self) -> u32 {
373 self.read_count.load(Ordering::Relaxed)
374 }
375
376 pub fn reset_read_count(&self) {
378 self.read_count.store(0, Ordering::Relaxed);
379 }
380
381 pub fn set_read_latency(&self, latency: Duration) {
386 self.read_latency_ms
387 .store(latency.as_millis() as u64, Ordering::Relaxed);
388 }
389
390 pub fn set_connect_latency(&self, latency: Duration) {
395 self.connect_latency_ms
396 .store(latency.as_millis() as u64, Ordering::Relaxed);
397 }
398
399 pub fn set_transient_failures(&self, count: u32) {
415 self.fail_count.store(count, Ordering::Relaxed);
416 self.remaining_failures.store(count, Ordering::Relaxed);
417 }
418
419 pub fn reset_transient_failures(&self) {
421 self.remaining_failures
422 .store(self.fail_count.load(Ordering::Relaxed), Ordering::Relaxed);
423 }
424
425 pub fn remaining_failures(&self) -> u32 {
427 self.remaining_failures.load(Ordering::Relaxed)
428 }
429}
430
431#[async_trait]
433impl AranetDevice for MockDevice {
434 async fn is_connected(&self) -> bool {
437 self.is_connected_sync()
438 }
439
440 async fn disconnect(&self) -> Result<()> {
441 MockDevice::disconnect(self).await
442 }
443
444 fn name(&self) -> Option<&str> {
447 Some(MockDevice::name(self))
448 }
449
450 fn address(&self) -> &str {
451 MockDevice::address(self)
452 }
453
454 fn device_type(&self) -> Option<DeviceType> {
455 Some(MockDevice::device_type(self))
456 }
457
458 async fn read_current(&self) -> Result<CurrentReading> {
461 MockDevice::read_current(self).await
462 }
463
464 async fn read_device_info(&self) -> Result<DeviceInfo> {
465 MockDevice::read_device_info(self).await
466 }
467
468 async fn read_rssi(&self) -> Result<i16> {
469 MockDevice::read_rssi(self).await
470 }
471
472 async fn read_battery(&self) -> Result<u8> {
475 MockDevice::read_battery(self).await
476 }
477
478 async fn get_history_info(&self) -> Result<crate::history::HistoryInfo> {
481 MockDevice::get_history_info(self).await
482 }
483
484 async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
485 MockDevice::download_history(self).await
486 }
487
488 async fn download_history_with_options(
489 &self,
490 options: HistoryOptions,
491 ) -> Result<Vec<HistoryRecord>> {
492 MockDevice::download_history_with_options(self, options).await
493 }
494
495 async fn get_interval(&self) -> Result<MeasurementInterval> {
498 MockDevice::get_interval(self).await
499 }
500
501 async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
502 MockDevice::set_interval(self, interval).await
503 }
504
505 async fn get_calibration(&self) -> Result<CalibrationData> {
506 MockDevice::get_calibration(self).await
507 }
508}
509
510#[derive(Debug)]
512pub struct MockDeviceBuilder {
513 name: String,
514 device_type: DeviceType,
515 co2: u16,
516 temperature: f32,
517 pressure: f32,
518 humidity: u8,
519 battery: u8,
520 status: Status,
521 auto_connect: bool,
522}
523
524impl Default for MockDeviceBuilder {
525 fn default() -> Self {
526 Self {
527 name: "Mock Aranet4".to_string(),
528 device_type: DeviceType::Aranet4,
529 co2: 800,
530 temperature: 22.5,
531 pressure: 1013.2,
532 humidity: 50,
533 battery: 85,
534 status: Status::Green,
535 auto_connect: true,
536 }
537 }
538}
539
540impl MockDeviceBuilder {
541 #[must_use]
543 pub fn new() -> Self {
544 Self::default()
545 }
546
547 #[must_use]
549 pub fn name(mut self, name: &str) -> Self {
550 self.name = name.to_string();
551 self
552 }
553
554 #[must_use]
556 pub fn device_type(mut self, device_type: DeviceType) -> Self {
557 self.device_type = device_type;
558 self
559 }
560
561 #[must_use]
563 pub fn co2(mut self, co2: u16) -> Self {
564 self.co2 = co2;
565 self
566 }
567
568 #[must_use]
570 pub fn temperature(mut self, temp: f32) -> Self {
571 self.temperature = temp;
572 self
573 }
574
575 #[must_use]
577 pub fn pressure(mut self, pressure: f32) -> Self {
578 self.pressure = pressure;
579 self
580 }
581
582 #[must_use]
584 pub fn humidity(mut self, humidity: u8) -> Self {
585 self.humidity = humidity;
586 self
587 }
588
589 #[must_use]
591 pub fn battery(mut self, battery: u8) -> Self {
592 self.battery = battery;
593 self
594 }
595
596 #[must_use]
598 pub fn status(mut self, status: Status) -> Self {
599 self.status = status;
600 self
601 }
602
603 #[must_use]
605 pub fn auto_connect(mut self, auto: bool) -> Self {
606 self.auto_connect = auto;
607 self
608 }
609
610 #[must_use]
615 pub fn build(self) -> MockDevice {
616 let reading = CurrentReading {
617 co2: self.co2,
618 temperature: self.temperature,
619 pressure: self.pressure,
620 humidity: self.humidity,
621 battery: self.battery,
622 status: self.status,
623 interval: 300,
624 age: 60,
625 captured_at: None,
626 radon: None,
627 radiation_rate: None,
628 radiation_total: None,
629 };
630
631 MockDevice {
632 name: self.name.clone(),
633 address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
634 device_type: self.device_type,
635 connected: AtomicBool::new(self.auto_connect),
636 current_reading: RwLock::new(reading),
637 device_info: RwLock::new(MockDevice::default_info(&self.name)),
638 history: RwLock::new(Vec::new()),
639 interval: RwLock::new(MeasurementInterval::FiveMinutes),
640 calibration: RwLock::new(CalibrationData::default()),
641 battery: RwLock::new(self.battery),
642 rssi: AtomicI16::new(-50),
643 read_count: AtomicU32::new(0),
644 should_fail: AtomicBool::new(false),
645 fail_message: RwLock::new("Mock failure".to_string()),
646 read_latency_ms: AtomicU64::new(0),
647 connect_latency_ms: AtomicU64::new(0),
648 fail_count: AtomicU32::new(0),
649 remaining_failures: AtomicU32::new(0),
650 }
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::traits::AranetDevice;
658
659 #[tokio::test]
660 async fn test_mock_device_connect() {
661 let device = MockDevice::new("Test", DeviceType::Aranet4);
662 assert!(!device.is_connected_sync());
663
664 device.connect().await.unwrap();
665 assert!(device.is_connected_sync());
666
667 device.disconnect().await.unwrap();
668 assert!(!device.is_connected_sync());
669 }
670
671 #[tokio::test]
672 async fn test_mock_device_read() {
673 let device = MockDeviceBuilder::new().co2(1200).temperature(25.0).build();
674
675 let reading = device.read_current().await.unwrap();
676 assert_eq!(reading.co2, 1200);
677 assert!((reading.temperature - 25.0).abs() < 0.01);
678 }
679
680 #[tokio::test]
681 async fn test_mock_device_fail() {
682 let device = MockDeviceBuilder::new().build();
683 device.set_should_fail(true, Some("Test error")).await;
684
685 let result = device.read_current().await;
686 assert!(result.is_err());
687 assert!(result.unwrap_err().to_string().contains("Test error"));
688 }
689
690 #[tokio::test]
691 async fn test_mock_device_not_connected() {
692 let device = MockDeviceBuilder::new().auto_connect(false).build();
693
694 let result = device.read_current().await;
695 assert!(matches!(result, Err(Error::NotConnected)));
696 }
697
698 #[test]
699 fn test_builder_defaults() {
700 let device = MockDeviceBuilder::new().build();
701 assert!(device.is_connected_sync());
702 assert_eq!(device.device_type(), DeviceType::Aranet4);
703 }
704
705 #[tokio::test]
706 async fn test_aranet_device_trait() {
707 let device = MockDeviceBuilder::new().co2(1000).build();
708
709 async fn check_via_trait<D: AranetDevice>(d: &D) -> u16 {
711 d.read_current().await.unwrap().co2
712 }
713
714 assert_eq!(check_via_trait(&device).await, 1000);
715 }
716}