aranet_core/
history.rs

1//! Historical data download.
2//!
3//! This module provides functionality to download historical sensor
4//! readings stored on an Aranet device.
5//!
6//! # Supported Devices
7//!
8//! | Device | History Support | Notes |
9//! |--------|-----------------|-------|
10//! | Aranet4 | Full | CO₂, temperature, pressure, humidity |
11//! | Aranet2 | Full | Temperature, humidity |
12//! | AranetRn+ (Radon) | Full | Radon, temperature, pressure, humidity |
13//! | Aranet Radiation | Partial | **History download not yet implemented** |
14//!
15//! **Note:** Aranet Radiation devices do not currently support history download.
16//! The `radiation_rate` and `radiation_total` fields in [`HistoryRecord`] are
17//! reserved for future implementation but are currently always `None`.
18//!
19//! # Index Convention
20//!
21//! **All history indices are 1-based**, following the Aranet device protocol:
22//! - Index 1 = oldest reading
23//! - Index N = newest reading (where N = total_readings)
24//!
25//! This matches the device's internal indexing. When specifying ranges:
26//! ```ignore
27//! let options = HistoryOptions {
28//!     start_index: Some(1),    // First reading
29//!     end_index: Some(100),    // 100th reading
30//!     ..Default::default()
31//! };
32//! ```
33//!
34//! # Protocols
35//!
36//! Aranet devices support two history protocols:
37//! - **V1**: Notification-based (older devices) - uses characteristic notifications
38//! - **V2**: Read-based (newer devices, preferred) - direct read/write operations
39
40use std::collections::BTreeMap;
41use std::sync::Arc;
42use std::time::Duration;
43
44use bytes::Buf;
45use time::OffsetDateTime;
46use tokio::time::sleep;
47use tracing::{debug, info, warn};
48
49use crate::commands::{HISTORY_V1_REQUEST, HISTORY_V2_REQUEST};
50use crate::device::Device;
51use crate::error::{Error, Result};
52use crate::uuid::{COMMAND, HISTORY_V2, READ_INTERVAL, SECONDS_SINCE_UPDATE, TOTAL_READINGS};
53use aranet_types::HistoryRecord;
54
55/// Progress information for history download.
56#[derive(Debug, Clone)]
57pub struct HistoryProgress {
58    /// Current parameter being downloaded.
59    pub current_param: HistoryParam,
60    /// Parameter index (1-based, e.g., 1 of 4).
61    pub param_index: usize,
62    /// Total number of parameters to download.
63    pub total_params: usize,
64    /// Number of values downloaded for current parameter.
65    pub values_downloaded: usize,
66    /// Total values to download for current parameter.
67    pub total_values: usize,
68    /// Overall progress (0.0 to 1.0).
69    pub overall_progress: f32,
70}
71
72impl HistoryProgress {
73    /// Create a new progress struct.
74    pub fn new(
75        param: HistoryParam,
76        param_idx: usize,
77        total_params: usize,
78        total_values: usize,
79    ) -> Self {
80        Self {
81            current_param: param,
82            param_index: param_idx,
83            total_params,
84            values_downloaded: 0,
85            total_values,
86            overall_progress: 0.0,
87        }
88    }
89
90    fn update(&mut self, values_downloaded: usize) {
91        self.values_downloaded = values_downloaded;
92        let param_progress = if self.total_values > 0 {
93            values_downloaded as f32 / self.total_values as f32
94        } else {
95            1.0
96        };
97        let base_progress = (self.param_index - 1) as f32 / self.total_params as f32;
98        let param_contribution = param_progress / self.total_params as f32;
99        self.overall_progress = base_progress + param_contribution;
100    }
101}
102
103/// Type alias for progress callback function.
104pub type ProgressCallback = Arc<dyn Fn(HistoryProgress) + Send + Sync>;
105
106/// Parameter types for history requests.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[repr(u8)]
109pub enum HistoryParam {
110    Temperature = 1,
111    Humidity = 2,
112    Pressure = 3,
113    Co2 = 4,
114    /// Humidity for Aranet2/Radon (different encoding).
115    Humidity2 = 5,
116    /// Radon concentration (Bq/m³) for AranetRn+.
117    Radon = 10,
118}
119
120/// Options for downloading history.
121///
122/// # Index Convention
123///
124/// Indices are **1-based** to match the Aranet device protocol:
125/// - `start_index: Some(1)` means the first (oldest) reading
126/// - `end_index: Some(100)` means the 100th reading
127/// - `start_index: None` defaults to 1 (beginning)
128/// - `end_index: None` defaults to total_readings (end)
129///
130/// # Progress Reporting
131///
132/// Use `with_progress` to receive updates during download:
133/// ```ignore
134/// let options = HistoryOptions::default()
135///     .with_progress(|p| println!("Progress: {:.1}%", p.overall_progress * 100.0));
136/// ```
137#[derive(Clone)]
138pub struct HistoryOptions {
139    /// Starting index (1-based, inclusive). If None, downloads from the beginning (index 1).
140    pub start_index: Option<u16>,
141    /// Ending index (1-based, inclusive). If None, downloads to the end (index = total_readings).
142    pub end_index: Option<u16>,
143    /// Delay between read operations to avoid overwhelming the device.
144    pub read_delay: Duration,
145    /// Progress callback (optional).
146    pub progress_callback: Option<ProgressCallback>,
147}
148
149impl std::fmt::Debug for HistoryOptions {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        f.debug_struct("HistoryOptions")
152            .field("start_index", &self.start_index)
153            .field("end_index", &self.end_index)
154            .field("read_delay", &self.read_delay)
155            .field("progress_callback", &self.progress_callback.is_some())
156            .finish()
157    }
158}
159
160impl Default for HistoryOptions {
161    fn default() -> Self {
162        Self {
163            start_index: None,
164            end_index: None,
165            read_delay: Duration::from_millis(50),
166            progress_callback: None,
167        }
168    }
169}
170
171impl HistoryOptions {
172    /// Create new history options with default settings.
173    #[must_use]
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Set the starting index (1-based).
179    #[must_use]
180    pub fn start_index(mut self, index: u16) -> Self {
181        self.start_index = Some(index);
182        self
183    }
184
185    /// Set the ending index (1-based).
186    #[must_use]
187    pub fn end_index(mut self, index: u16) -> Self {
188        self.end_index = Some(index);
189        self
190    }
191
192    /// Set the delay between read operations.
193    #[must_use]
194    pub fn read_delay(mut self, delay: Duration) -> Self {
195        self.read_delay = delay;
196        self
197    }
198
199    /// Set a progress callback.
200    #[must_use]
201    pub fn with_progress<F>(mut self, callback: F) -> Self
202    where
203        F: Fn(HistoryProgress) + Send + Sync + 'static,
204    {
205        self.progress_callback = Some(Arc::new(callback));
206        self
207    }
208
209    /// Report progress if a callback is set.
210    pub fn report_progress(&self, progress: &HistoryProgress) {
211        if let Some(cb) = &self.progress_callback {
212            cb(progress.clone());
213        }
214    }
215}
216
217/// Information about the device's stored history.
218#[derive(Debug, Clone)]
219pub struct HistoryInfo {
220    /// Total number of readings stored.
221    pub total_readings: u16,
222    /// Measurement interval in seconds.
223    pub interval_seconds: u16,
224    /// Seconds since the last reading.
225    pub seconds_since_update: u16,
226}
227
228impl Device {
229    /// Get information about the stored history.
230    pub async fn get_history_info(&self) -> Result<HistoryInfo> {
231        // Read total readings count
232        let total_data = self.read_characteristic(TOTAL_READINGS).await?;
233        let total_readings = if total_data.len() >= 2 {
234            u16::from_le_bytes([total_data[0], total_data[1]])
235        } else {
236            return Err(Error::InvalidData(
237                "Invalid total readings data".to_string(),
238            ));
239        };
240
241        // Read interval
242        let interval_data = self.read_characteristic(READ_INTERVAL).await?;
243        let interval_seconds = if interval_data.len() >= 2 {
244            u16::from_le_bytes([interval_data[0], interval_data[1]])
245        } else {
246            return Err(Error::InvalidData("Invalid interval data".to_string()));
247        };
248
249        // Read seconds since update
250        let age_data = self.read_characteristic(SECONDS_SINCE_UPDATE).await?;
251        let seconds_since_update = if age_data.len() >= 2 {
252            u16::from_le_bytes([age_data[0], age_data[1]])
253        } else {
254            0
255        };
256
257        Ok(HistoryInfo {
258            total_readings,
259            interval_seconds,
260            seconds_since_update,
261        })
262    }
263
264    /// Download all historical readings from the device.
265    pub async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
266        self.download_history_with_options(HistoryOptions::default())
267            .await
268    }
269
270    /// Download historical readings with custom options.
271    ///
272    /// # Device Support
273    ///
274    /// - **Aranet4**: Downloads CO₂, temperature, pressure, humidity
275    /// - **Aranet2**: Downloads temperature, humidity
276    /// - **AranetRn+ (Radon)**: Downloads radon, temperature, pressure, humidity
277    /// - **Aranet Radiation**: **Not yet supported** - will return Aranet4-style records
278    ///   with placeholder values. The `radiation_rate` and `radiation_total` fields
279    ///   in the returned records will be `None`.
280    pub async fn download_history_with_options(
281        &self,
282        options: HistoryOptions,
283    ) -> Result<Vec<HistoryRecord>> {
284        use aranet_types::DeviceType;
285
286        let info = self.get_history_info().await?;
287        info!(
288            "Device has {} readings, interval {}s, last update {}s ago",
289            info.total_readings, info.interval_seconds, info.seconds_since_update
290        );
291
292        if info.total_readings == 0 {
293            return Ok(Vec::new());
294        }
295
296        let start_idx = options.start_index.unwrap_or(1);
297        let end_idx = options.end_index.unwrap_or(info.total_readings);
298
299        // Check if this is a radon device
300        let is_radon = matches!(self.device_type(), Some(DeviceType::AranetRadon));
301
302        if is_radon {
303            // For radon devices, download radon instead of CO2, and use Humidity2
304            self.download_radon_history_internal(&info, start_idx, end_idx, &options)
305                .await
306        } else {
307            // For Aranet4, download CO2 and standard humidity
308            self.download_aranet4_history_internal(&info, start_idx, end_idx, &options)
309                .await
310        }
311    }
312
313    /// Download history for Aranet4 devices (CO2, temp, pressure, humidity).
314    async fn download_aranet4_history_internal(
315        &self,
316        info: &HistoryInfo,
317        start_idx: u16,
318        end_idx: u16,
319        options: &HistoryOptions,
320    ) -> Result<Vec<HistoryRecord>> {
321        let total_values = (end_idx - start_idx + 1) as usize;
322
323        // Download each parameter type with progress reporting
324        let mut progress = HistoryProgress::new(HistoryParam::Co2, 1, 4, total_values);
325        options.report_progress(&progress);
326
327        let co2_values = self
328            .download_param_history_with_progress(
329                HistoryParam::Co2,
330                start_idx,
331                end_idx,
332                options.read_delay,
333                |downloaded| {
334                    progress.update(downloaded);
335                    options.report_progress(&progress);
336                },
337            )
338            .await?;
339
340        progress = HistoryProgress::new(HistoryParam::Temperature, 2, 4, total_values);
341        options.report_progress(&progress);
342
343        let temp_values = self
344            .download_param_history_with_progress(
345                HistoryParam::Temperature,
346                start_idx,
347                end_idx,
348                options.read_delay,
349                |downloaded| {
350                    progress.update(downloaded);
351                    options.report_progress(&progress);
352                },
353            )
354            .await?;
355
356        progress = HistoryProgress::new(HistoryParam::Pressure, 3, 4, total_values);
357        options.report_progress(&progress);
358
359        let pressure_values = self
360            .download_param_history_with_progress(
361                HistoryParam::Pressure,
362                start_idx,
363                end_idx,
364                options.read_delay,
365                |downloaded| {
366                    progress.update(downloaded);
367                    options.report_progress(&progress);
368                },
369            )
370            .await?;
371
372        progress = HistoryProgress::new(HistoryParam::Humidity, 4, 4, total_values);
373        options.report_progress(&progress);
374
375        let humidity_values = self
376            .download_param_history_with_progress(
377                HistoryParam::Humidity,
378                start_idx,
379                end_idx,
380                options.read_delay,
381                |downloaded| {
382                    progress.update(downloaded);
383                    options.report_progress(&progress);
384                },
385            )
386            .await?;
387
388        // Calculate timestamps for each record
389        let now = OffsetDateTime::now_utc();
390        let latest_reading_time = now - time::Duration::seconds(info.seconds_since_update as i64);
391
392        // Build history records by combining all parameters
393        let mut records = Vec::new();
394        let count = co2_values.len();
395
396        for i in 0..count {
397            // Calculate timestamp: most recent reading is at the end
398            let readings_ago = (count - 1 - i) as i64;
399            let timestamp = latest_reading_time
400                - time::Duration::seconds(readings_ago * info.interval_seconds as i64);
401
402            let record = HistoryRecord {
403                timestamp,
404                co2: co2_values.get(i).copied().unwrap_or(0),
405                temperature: raw_to_temperature(temp_values.get(i).copied().unwrap_or(0)),
406                pressure: raw_to_pressure(pressure_values.get(i).copied().unwrap_or(0)),
407                humidity: humidity_values.get(i).copied().unwrap_or(0) as u8,
408                radon: None,
409                radiation_rate: None,
410                radiation_total: None,
411            };
412            records.push(record);
413        }
414
415        info!("Downloaded {} history records", records.len());
416        Ok(records)
417    }
418
419    /// Download history for AranetRn+ devices (radon, temp, pressure, humidity).
420    async fn download_radon_history_internal(
421        &self,
422        info: &HistoryInfo,
423        start_idx: u16,
424        end_idx: u16,
425        options: &HistoryOptions,
426    ) -> Result<Vec<HistoryRecord>> {
427        let total_values = (end_idx - start_idx + 1) as usize;
428
429        // Download radon values (4 bytes each)
430        let mut progress = HistoryProgress::new(HistoryParam::Radon, 1, 4, total_values);
431        options.report_progress(&progress);
432
433        let radon_values = self
434            .download_param_history_u32_with_progress(
435                HistoryParam::Radon,
436                start_idx,
437                end_idx,
438                options.read_delay,
439                |downloaded| {
440                    progress.update(downloaded);
441                    options.report_progress(&progress);
442                },
443            )
444            .await?;
445
446        progress = HistoryProgress::new(HistoryParam::Temperature, 2, 4, total_values);
447        options.report_progress(&progress);
448
449        let temp_values = self
450            .download_param_history_with_progress(
451                HistoryParam::Temperature,
452                start_idx,
453                end_idx,
454                options.read_delay,
455                |downloaded| {
456                    progress.update(downloaded);
457                    options.report_progress(&progress);
458                },
459            )
460            .await?;
461
462        progress = HistoryProgress::new(HistoryParam::Pressure, 3, 4, total_values);
463        options.report_progress(&progress);
464
465        let pressure_values = self
466            .download_param_history_with_progress(
467                HistoryParam::Pressure,
468                start_idx,
469                end_idx,
470                options.read_delay,
471                |downloaded| {
472                    progress.update(downloaded);
473                    options.report_progress(&progress);
474                },
475            )
476            .await?;
477
478        // Radon devices use Humidity2 (different encoding, 2 bytes, divide by 10)
479        progress = HistoryProgress::new(HistoryParam::Humidity2, 4, 4, total_values);
480        options.report_progress(&progress);
481
482        let humidity_values = self
483            .download_param_history_with_progress(
484                HistoryParam::Humidity2,
485                start_idx,
486                end_idx,
487                options.read_delay,
488                |downloaded| {
489                    progress.update(downloaded);
490                    options.report_progress(&progress);
491                },
492            )
493            .await?;
494
495        // Calculate timestamps for each record
496        let now = OffsetDateTime::now_utc();
497        let latest_reading_time = now - time::Duration::seconds(info.seconds_since_update as i64);
498
499        // Build history records by combining all parameters
500        let mut records = Vec::new();
501        let count = radon_values.len();
502
503        for i in 0..count {
504            // Calculate timestamp: most recent reading is at the end
505            let readings_ago = (count - 1 - i) as i64;
506            let timestamp = latest_reading_time
507                - time::Duration::seconds(readings_ago * info.interval_seconds as i64);
508
509            // Humidity2 is stored as tenths of a percent
510            let humidity_raw = humidity_values.get(i).copied().unwrap_or(0);
511            let humidity = (humidity_raw / 10).min(100) as u8;
512
513            let record = HistoryRecord {
514                timestamp,
515                co2: 0, // Not applicable for radon devices
516                temperature: raw_to_temperature(temp_values.get(i).copied().unwrap_or(0)),
517                pressure: raw_to_pressure(pressure_values.get(i).copied().unwrap_or(0)),
518                humidity,
519                radon: Some(radon_values.get(i).copied().unwrap_or(0)),
520                radiation_rate: None,
521                radiation_total: None,
522            };
523            records.push(record);
524        }
525
526        info!("Downloaded {} radon history records", records.len());
527        Ok(records)
528    }
529
530    /// Download a single parameter's history using V2 protocol with progress callback.
531    ///
532    /// This is a generic implementation that handles different value sizes:
533    /// - 1 byte: humidity
534    /// - 2 bytes: CO2, temperature, pressure, humidity2
535    /// - 4 bytes: radon
536    #[allow(clippy::too_many_arguments)]
537    async fn download_param_history_generic_with_progress<T, F>(
538        &self,
539        param: HistoryParam,
540        start_idx: u16,
541        end_idx: u16,
542        read_delay: Duration,
543        value_parser: impl Fn(&[u8], usize) -> Option<T>,
544        value_size: usize,
545        mut on_progress: F,
546    ) -> Result<Vec<T>>
547    where
548        T: Default + Clone,
549        F: FnMut(usize),
550    {
551        debug!(
552            "Downloading {:?} history from {} to {} (value_size={})",
553            param, start_idx, end_idx, value_size
554        );
555
556        let mut values: BTreeMap<u16, T> = BTreeMap::new();
557        let mut current_idx = start_idx;
558
559        while current_idx <= end_idx {
560            // Send V2 history request using command constant
561            let cmd = [
562                HISTORY_V2_REQUEST,
563                param as u8,
564                (current_idx & 0xFF) as u8,
565                ((current_idx >> 8) & 0xFF) as u8,
566            ];
567
568            self.write_characteristic(COMMAND, &cmd).await?;
569            sleep(read_delay).await;
570
571            // Read response
572            let response = self.read_characteristic(HISTORY_V2).await?;
573
574            // V2 response format (10-byte header):
575            // Byte 0: param (1 byte)
576            // Bytes 1-2: interval (2 bytes, little-endian)
577            // Bytes 3-4: total_readings (2 bytes, little-endian)
578            // Bytes 5-6: ago (2 bytes, little-endian)
579            // Bytes 7-8: start index (2 bytes, little-endian)
580            // Byte 9: count (1 byte)
581            // Bytes 10+: data values
582            if response.len() < 10 {
583                warn!(
584                    "Invalid history response: too short ({} bytes)",
585                    response.len()
586                );
587                break;
588            }
589
590            let resp_param = response[0];
591            if resp_param != param as u8 {
592                warn!("Unexpected parameter in response: {}", resp_param);
593                // Wait and retry - device may not have processed command yet
594                sleep(read_delay).await;
595                continue;
596            }
597
598            // Parse header
599            let resp_start = u16::from_le_bytes([response[7], response[8]]);
600            let resp_count = response[9] as usize;
601
602            debug!(
603                "History response: param={}, start={}, count={}",
604                resp_param, resp_start, resp_count
605            );
606
607            // Check if we've reached the end (count == 0)
608            if resp_count == 0 {
609                debug!("Reached end of history (count=0)");
610                break;
611            }
612
613            // Parse data values
614            let data = &response[10..];
615            let num_values = (data.len() / value_size).min(resp_count);
616
617            for i in 0..num_values {
618                let idx = resp_start + i as u16;
619                if idx > end_idx {
620                    break;
621                }
622                if let Some(value) = value_parser(data, i) {
623                    values.insert(idx, value);
624                }
625            }
626
627            current_idx = resp_start + num_values as u16;
628            debug!(
629                "Downloaded {} values, next index: {}",
630                num_values, current_idx
631            );
632
633            // Report progress
634            on_progress(values.len());
635
636            // Check if we've downloaded all available data
637            if (resp_start as usize + resp_count) >= end_idx as usize {
638                debug!("Reached end of requested range");
639                break;
640            }
641        }
642
643        // Convert to ordered vector (BTreeMap already maintains order)
644        Ok(values.into_values().collect())
645    }
646
647    /// Download a single parameter's history using V2 protocol (u16 values) with progress.
648    async fn download_param_history_with_progress<F>(
649        &self,
650        param: HistoryParam,
651        start_idx: u16,
652        end_idx: u16,
653        read_delay: Duration,
654        on_progress: F,
655    ) -> Result<Vec<u16>>
656    where
657        F: FnMut(usize),
658    {
659        let value_size = if param == HistoryParam::Humidity {
660            1
661        } else {
662            2
663        };
664
665        self.download_param_history_generic_with_progress(
666            param,
667            start_idx,
668            end_idx,
669            read_delay,
670            |data, i| {
671                if param == HistoryParam::Humidity {
672                    data.get(i).map(|&b| b as u16)
673                } else {
674                    let offset = i * 2;
675                    if offset + 1 < data.len() {
676                        Some(u16::from_le_bytes([data[offset], data[offset + 1]]))
677                    } else {
678                        None
679                    }
680                }
681            },
682            value_size,
683            on_progress,
684        )
685        .await
686    }
687
688    /// Download a single parameter's history using V2 protocol (u32 values) with progress.
689    async fn download_param_history_u32_with_progress<F>(
690        &self,
691        param: HistoryParam,
692        start_idx: u16,
693        end_idx: u16,
694        read_delay: Duration,
695        on_progress: F,
696    ) -> Result<Vec<u32>>
697    where
698        F: FnMut(usize),
699    {
700        self.download_param_history_generic_with_progress(
701            param,
702            start_idx,
703            end_idx,
704            read_delay,
705            |data, i| {
706                let offset = i * 4;
707                if offset + 3 < data.len() {
708                    Some(u32::from_le_bytes([
709                        data[offset],
710                        data[offset + 1],
711                        data[offset + 2],
712                        data[offset + 3],
713                    ]))
714                } else {
715                    None
716                }
717            },
718            4,
719            on_progress,
720        )
721        .await
722    }
723
724    /// Download history using V1 protocol (notification-based).
725    ///
726    /// This is used for older devices that don't support the V2 read-based protocol.
727    /// V1 uses notifications on the HISTORY_V1 characteristic.
728    pub async fn download_history_v1(&self) -> Result<Vec<HistoryRecord>> {
729        use crate::uuid::HISTORY_V1;
730        use tokio::sync::mpsc;
731
732        let info = self.get_history_info().await?;
733        info!(
734            "V1 download: {} readings, interval {}s",
735            info.total_readings, info.interval_seconds
736        );
737
738        if info.total_readings == 0 {
739            return Ok(Vec::new());
740        }
741
742        // Subscribe to notifications
743        let (tx, mut rx) = mpsc::channel::<Vec<u8>>(256);
744
745        // Set up notification handler
746        self.subscribe_to_notifications(HISTORY_V1, move |data| {
747            let _ = tx.try_send(data.to_vec());
748        })
749        .await?;
750
751        // Request history for each parameter
752        let mut co2_values = Vec::new();
753        let mut temp_values = Vec::new();
754        let mut pressure_values = Vec::new();
755        let mut humidity_values = Vec::new();
756
757        for param in [
758            HistoryParam::Co2,
759            HistoryParam::Temperature,
760            HistoryParam::Pressure,
761            HistoryParam::Humidity,
762        ] {
763            // Send V1 history request using command constant
764            let cmd = [
765                HISTORY_V1_REQUEST,
766                param as u8,
767                0x01,
768                0x00,
769                (info.total_readings & 0xFF) as u8,
770                ((info.total_readings >> 8) & 0xFF) as u8,
771            ];
772
773            self.write_characteristic(COMMAND, &cmd).await?;
774
775            // Collect notifications until we have all values
776            let mut values = Vec::new();
777            let expected = info.total_readings as usize;
778
779            while values.len() < expected {
780                match tokio::time::timeout(Duration::from_secs(5), rx.recv()).await {
781                    Ok(Some(data)) => {
782                        // Parse notification data
783                        if data.len() >= 3 {
784                            let resp_param = data[0];
785                            if resp_param == param as u8 {
786                                let mut buf = &data[3..];
787                                while buf.len() >= 2 && values.len() < expected {
788                                    values.push(buf.get_u16_le());
789                                }
790                            }
791                        }
792                    }
793                    Ok(None) => break,
794                    Err(_) => {
795                        warn!("Timeout waiting for V1 history notification");
796                        break;
797                    }
798                }
799            }
800
801            match param {
802                HistoryParam::Co2 => co2_values = values,
803                HistoryParam::Temperature => temp_values = values,
804                HistoryParam::Pressure => pressure_values = values,
805                HistoryParam::Humidity => humidity_values = values,
806                // V1 protocol doesn't support radon or humidity2
807                HistoryParam::Humidity2 | HistoryParam::Radon => {}
808            }
809        }
810
811        // Unsubscribe from notifications
812        self.unsubscribe_from_notifications(HISTORY_V1).await?;
813
814        // Build history records
815        let now = OffsetDateTime::now_utc();
816        let latest_reading_time = now - time::Duration::seconds(info.seconds_since_update as i64);
817
818        let mut records = Vec::new();
819        let count = co2_values.len();
820
821        for i in 0..count {
822            let readings_ago = (count - 1 - i) as i64;
823            let timestamp = latest_reading_time
824                - time::Duration::seconds(readings_ago * info.interval_seconds as i64);
825
826            let record = HistoryRecord {
827                timestamp,
828                co2: co2_values.get(i).copied().unwrap_or(0),
829                temperature: raw_to_temperature(temp_values.get(i).copied().unwrap_or(0)),
830                pressure: raw_to_pressure(pressure_values.get(i).copied().unwrap_or(0)),
831                humidity: humidity_values.get(i).copied().unwrap_or(0) as u8,
832                radon: None,
833                radiation_rate: None,
834                radiation_total: None,
835            };
836            records.push(record);
837        }
838
839        info!("V1 download complete: {} records", records.len());
840        Ok(records)
841    }
842}
843
844/// Convert raw temperature value to Celsius.
845pub fn raw_to_temperature(raw: u16) -> f32 {
846    raw as f32 / 20.0
847}
848
849/// Convert raw pressure value to hPa.
850pub fn raw_to_pressure(raw: u16) -> f32 {
851    raw as f32 / 10.0
852}
853
854// NOTE: The HistoryValueConverter trait was removed as it was dead code.
855// Use the standalone functions raw_to_temperature, raw_to_pressure, etc. directly.
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    // --- raw_to_temperature tests ---
862
863    #[test]
864    fn test_raw_to_temperature_typical_values() {
865        // 22.5°C = 450 raw (450/20 = 22.5)
866        assert!((raw_to_temperature(450) - 22.5).abs() < 0.001);
867
868        // 20.0°C = 400 raw
869        assert!((raw_to_temperature(400) - 20.0).abs() < 0.001);
870
871        // 25.0°C = 500 raw
872        assert!((raw_to_temperature(500) - 25.0).abs() < 0.001);
873    }
874
875    #[test]
876    fn test_raw_to_temperature_edge_cases() {
877        // 0°C = 0 raw
878        assert!((raw_to_temperature(0) - 0.0).abs() < 0.001);
879
880        // Very cold: -10°C would be negative, but raw is u16 so minimum is 0
881        // Raw values represent actual temperature * 20
882
883        // Very hot: 50°C = 1000 raw
884        assert!((raw_to_temperature(1000) - 50.0).abs() < 0.001);
885
886        // Maximum u16 would be 65535/20 = 3276.75°C (unrealistic but tests overflow handling)
887        assert!((raw_to_temperature(u16::MAX) - 3276.75).abs() < 0.01);
888    }
889
890    #[test]
891    fn test_raw_to_temperature_precision() {
892        // Test fractional values
893        // 22.55°C = 451 raw
894        assert!((raw_to_temperature(451) - 22.55).abs() < 0.001);
895
896        // 22.05°C = 441 raw
897        assert!((raw_to_temperature(441) - 22.05).abs() < 0.001);
898    }
899
900    // --- raw_to_pressure tests ---
901
902    #[test]
903    fn test_raw_to_pressure_typical_values() {
904        // 1013.2 hPa = 10132 raw
905        assert!((raw_to_pressure(10132) - 1013.2).abs() < 0.01);
906
907        // 1000.0 hPa = 10000 raw
908        assert!((raw_to_pressure(10000) - 1000.0).abs() < 0.01);
909
910        // 1050.0 hPa = 10500 raw
911        assert!((raw_to_pressure(10500) - 1050.0).abs() < 0.01);
912    }
913
914    #[test]
915    fn test_raw_to_pressure_edge_cases() {
916        // 0 hPa = 0 raw
917        assert!((raw_to_pressure(0) - 0.0).abs() < 0.01);
918
919        // Low pressure: 950 hPa = 9500 raw
920        assert!((raw_to_pressure(9500) - 950.0).abs() < 0.01);
921
922        // High pressure: 1100 hPa = 11000 raw
923        assert!((raw_to_pressure(11000) - 1100.0).abs() < 0.01);
924
925        // Maximum u16 would be 65535/10 = 6553.5 hPa (unrealistic but tests bounds)
926        assert!((raw_to_pressure(u16::MAX) - 6553.5).abs() < 0.1);
927    }
928
929    // --- HistoryParam tests ---
930
931    #[test]
932    fn test_history_param_values() {
933        assert_eq!(HistoryParam::Temperature as u8, 1);
934        assert_eq!(HistoryParam::Humidity as u8, 2);
935        assert_eq!(HistoryParam::Pressure as u8, 3);
936        assert_eq!(HistoryParam::Co2 as u8, 4);
937    }
938
939    #[test]
940    fn test_history_param_debug() {
941        assert_eq!(format!("{:?}", HistoryParam::Temperature), "Temperature");
942        assert_eq!(format!("{:?}", HistoryParam::Co2), "Co2");
943    }
944
945    // --- HistoryOptions tests ---
946
947    #[test]
948    fn test_history_options_default() {
949        let options = HistoryOptions::default();
950
951        assert!(options.start_index.is_none());
952        assert!(options.end_index.is_none());
953        assert_eq!(options.read_delay, Duration::from_millis(50));
954    }
955
956    #[test]
957    fn test_history_options_custom() {
958        let options = HistoryOptions::new()
959            .start_index(10)
960            .end_index(100)
961            .read_delay(Duration::from_millis(100));
962
963        assert_eq!(options.start_index, Some(10));
964        assert_eq!(options.end_index, Some(100));
965        assert_eq!(options.read_delay, Duration::from_millis(100));
966    }
967
968    #[test]
969    fn test_history_options_with_progress() {
970        use std::sync::Arc;
971        use std::sync::atomic::{AtomicUsize, Ordering};
972
973        let call_count = Arc::new(AtomicUsize::new(0));
974        let call_count_clone = Arc::clone(&call_count);
975
976        let options = HistoryOptions::new().with_progress(move |_progress| {
977            call_count_clone.fetch_add(1, Ordering::SeqCst);
978        });
979
980        assert!(options.progress_callback.is_some());
981
982        // Test that the callback can be invoked
983        let progress = HistoryProgress::new(HistoryParam::Co2, 1, 4, 100);
984        options.report_progress(&progress);
985        assert_eq!(call_count.load(Ordering::SeqCst), 1);
986    }
987
988    // --- HistoryInfo tests ---
989
990    #[test]
991    fn test_history_info_creation() {
992        let info = HistoryInfo {
993            total_readings: 1000,
994            interval_seconds: 300,
995            seconds_since_update: 120,
996        };
997
998        assert_eq!(info.total_readings, 1000);
999        assert_eq!(info.interval_seconds, 300);
1000        assert_eq!(info.seconds_since_update, 120);
1001    }
1002
1003    #[test]
1004    fn test_history_info_debug() {
1005        let info = HistoryInfo {
1006            total_readings: 500,
1007            interval_seconds: 60,
1008            seconds_since_update: 30,
1009        };
1010
1011        let debug_str = format!("{:?}", info);
1012        assert!(debug_str.contains("total_readings"));
1013        assert!(debug_str.contains("500"));
1014    }
1015}