aranet_core/
thresholds.rs1use serde::{Deserialize, Serialize};
23
24use aranet_types::CurrentReading;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum Co2Level {
29 Excellent,
31 Good,
33 Moderate,
35 Poor,
37 VeryPoor,
39 Hazardous,
41}
42
43impl Co2Level {
44 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ThresholdConfig {
71 pub excellent_max: u16,
73 pub good_max: u16,
75 pub moderate_max: u16,
77 pub poor_max: u16,
79 pub very_poor_max: u16,
81 }
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 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 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#[derive(Debug, Clone, Default)]
122pub struct Thresholds {
123 config: ThresholdConfig,
124}
125
126impl Thresholds {
127 pub fn new(config: ThresholdConfig) -> Self {
129 Self { config }
130 }
131
132 pub fn strict() -> Self {
134 Self::new(ThresholdConfig::strict())
135 }
136
137 pub fn relaxed() -> Self {
139 Self::new(ThresholdConfig::relaxed())
140 }
141
142 pub fn config(&self) -> &ThresholdConfig {
144 &self.config
145 }
146
147 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 pub fn evaluate_reading(&self, reading: &CurrentReading) -> Co2Level {
166 self.evaluate_co2(reading.co2)
167 }
168
169 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, }
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 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}