aranet_core/
thresholds.rs

1//! CO2 level thresholds and categorization.
2//!
3//! This module provides configurable thresholds for categorizing CO2 levels
4//! and other sensor readings into actionable categories.
5//!
6//! # Example
7//!
8//! ```
9//! use aranet_core::{Thresholds, Co2Level};
10//!
11//! // Use default thresholds
12//! let thresholds = Thresholds::default();
13//!
14//! // Evaluate a CO2 reading
15//! let level = thresholds.evaluate_co2(800);
16//! assert_eq!(level, Co2Level::Good);
17//!
18//! // Get action recommendation
19//! println!("{}", level.action());
20//! ```
21
22use serde::{Deserialize, Serialize};
23
24use aranet_types::CurrentReading;
25
26/// CO2 level category based on concentration.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum Co2Level {
29    /// Excellent air quality (typically < 600 ppm).
30    Excellent,
31    /// Good air quality (typically 600-800 ppm).
32    Good,
33    /// Moderate air quality (typically 800-1000 ppm).
34    Moderate,
35    /// Poor air quality (typically 1000-1500 ppm).
36    Poor,
37    /// Very poor air quality (typically 1500-2000 ppm).
38    VeryPoor,
39    /// Hazardous air quality (typically > 2000 ppm).
40    Hazardous,
41}
42
43impl Co2Level {
44    /// Get a human-readable description of the CO2 level.
45    pub fn description(&self) -> &'static str {
46        match self {
47            Co2Level::Excellent => "Excellent - outdoor air quality",
48            Co2Level::Good => "Good - typical indoor air",
49            Co2Level::Moderate => "Moderate - consider ventilation",
50            Co2Level::Poor => "Poor - ventilation recommended",
51            Co2Level::VeryPoor => "Very Poor - ventilate immediately",
52            Co2Level::Hazardous => "Hazardous - leave area if possible",
53        }
54    }
55
56    /// Get the suggested action for this CO2 level.
57    pub fn action(&self) -> &'static str {
58        match self {
59            Co2Level::Excellent | Co2Level::Good => "No action needed",
60            Co2Level::Moderate => "Consider opening windows",
61            Co2Level::Poor => "Open windows or turn on ventilation",
62            Co2Level::VeryPoor => "Immediate ventilation required",
63            Co2Level::Hazardous => "Leave area and ventilate thoroughly",
64        }
65    }
66}
67
68/// Configuration for CO2 thresholds.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ThresholdConfig {
71    /// Upper bound for Excellent level.
72    pub excellent_max: u16,
73    /// Upper bound for Good level.
74    pub good_max: u16,
75    /// Upper bound for Moderate level.
76    pub moderate_max: u16,
77    /// Upper bound for Poor level.
78    pub poor_max: u16,
79    /// Upper bound for Very Poor level.
80    pub very_poor_max: u16,
81    // Above very_poor_max is Hazardous
82}
83
84impl Default for ThresholdConfig {
85    fn default() -> Self {
86        Self {
87            excellent_max: 600,
88            good_max: 800,
89            moderate_max: 1000,
90            poor_max: 1500,
91            very_poor_max: 2000,
92        }
93    }
94}
95
96impl ThresholdConfig {
97    /// Create strict thresholds suitable for sensitive environments.
98    pub fn strict() -> Self {
99        Self {
100            excellent_max: 450,
101            good_max: 600,
102            moderate_max: 800,
103            poor_max: 1000,
104            very_poor_max: 1500,
105        }
106    }
107
108    /// Create relaxed thresholds for industrial environments.
109    pub fn relaxed() -> Self {
110        Self {
111            excellent_max: 800,
112            good_max: 1000,
113            moderate_max: 1500,
114            poor_max: 2500,
115            very_poor_max: 5000,
116        }
117    }
118}
119
120/// Threshold evaluator for sensor readings.
121#[derive(Debug, Clone, Default)]
122pub struct Thresholds {
123    config: ThresholdConfig,
124}
125
126impl Thresholds {
127    /// Create a new threshold evaluator with the given configuration.
128    pub fn new(config: ThresholdConfig) -> Self {
129        Self { config }
130    }
131
132    /// Create a threshold evaluator with strict thresholds.
133    pub fn strict() -> Self {
134        Self::new(ThresholdConfig::strict())
135    }
136
137    /// Create a threshold evaluator with relaxed thresholds.
138    pub fn relaxed() -> Self {
139        Self::new(ThresholdConfig::relaxed())
140    }
141
142    /// Get the configuration.
143    pub fn config(&self) -> &ThresholdConfig {
144        &self.config
145    }
146
147    /// Evaluate the CO2 level from a reading.
148    pub fn evaluate_co2(&self, co2_ppm: u16) -> Co2Level {
149        if co2_ppm <= self.config.excellent_max {
150            Co2Level::Excellent
151        } else if co2_ppm <= self.config.good_max {
152            Co2Level::Good
153        } else if co2_ppm <= self.config.moderate_max {
154            Co2Level::Moderate
155        } else if co2_ppm <= self.config.poor_max {
156            Co2Level::Poor
157        } else if co2_ppm <= self.config.very_poor_max {
158            Co2Level::VeryPoor
159        } else {
160            Co2Level::Hazardous
161        }
162    }
163
164    /// Evaluate the CO2 level from a CurrentReading.
165    pub fn evaluate_reading(&self, reading: &CurrentReading) -> Co2Level {
166        self.evaluate_co2(reading.co2)
167    }
168
169    /// Check if a CO2 reading exceeds a specific threshold.
170    pub fn exceeds_threshold(&self, co2_ppm: u16, level: Co2Level) -> bool {
171        match level {
172            Co2Level::Excellent => co2_ppm > self.config.excellent_max,
173            Co2Level::Good => co2_ppm > self.config.good_max,
174            Co2Level::Moderate => co2_ppm > self.config.moderate_max,
175            Co2Level::Poor => co2_ppm > self.config.poor_max,
176            Co2Level::VeryPoor => co2_ppm > self.config.very_poor_max,
177            Co2Level::Hazardous => true, // Already at highest level
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_default_thresholds() {
188        let t = Thresholds::default();
189        assert_eq!(t.evaluate_co2(400), Co2Level::Excellent);
190        assert_eq!(t.evaluate_co2(700), Co2Level::Good);
191        assert_eq!(t.evaluate_co2(900), Co2Level::Moderate);
192        assert_eq!(t.evaluate_co2(1200), Co2Level::Poor);
193        assert_eq!(t.evaluate_co2(1800), Co2Level::VeryPoor);
194        assert_eq!(t.evaluate_co2(2500), Co2Level::Hazardous);
195    }
196
197    #[test]
198    fn test_strict_thresholds() {
199        let t = Thresholds::strict();
200        assert_eq!(t.evaluate_co2(400), Co2Level::Excellent);
201        assert_eq!(t.evaluate_co2(500), Co2Level::Good);
202        assert_eq!(t.evaluate_co2(700), Co2Level::Moderate);
203        assert_eq!(t.evaluate_co2(900), Co2Level::Poor);
204    }
205
206    #[test]
207    fn test_relaxed_thresholds() {
208        let t = Thresholds::relaxed();
209        assert_eq!(t.evaluate_co2(700), Co2Level::Excellent);
210        assert_eq!(t.evaluate_co2(900), Co2Level::Good);
211        assert_eq!(t.evaluate_co2(1200), Co2Level::Moderate);
212    }
213
214    #[test]
215    fn test_boundary_values() {
216        let t = Thresholds::default();
217        // Exact boundaries
218        assert_eq!(t.evaluate_co2(600), Co2Level::Excellent);
219        assert_eq!(t.evaluate_co2(601), Co2Level::Good);
220        assert_eq!(t.evaluate_co2(800), Co2Level::Good);
221        assert_eq!(t.evaluate_co2(801), Co2Level::Moderate);
222    }
223
224    #[test]
225    fn test_co2_level_descriptions() {
226        assert!(Co2Level::Excellent.description().contains("Excellent"));
227        assert!(Co2Level::Hazardous.description().contains("Hazardous"));
228    }
229
230    #[test]
231    fn test_co2_level_actions() {
232        assert!(Co2Level::Excellent.action().contains("No action"));
233        assert!(Co2Level::VeryPoor.action().contains("Immediate"));
234    }
235
236    #[test]
237    fn test_exceeds_threshold() {
238        let t = Thresholds::default();
239        assert!(!t.exceeds_threshold(600, Co2Level::Excellent));
240        assert!(t.exceeds_threshold(601, Co2Level::Excellent));
241        assert!(!t.exceeds_threshold(1000, Co2Level::Moderate));
242        assert!(t.exceeds_threshold(1001, Co2Level::Moderate));
243    }
244}