Skip to main content

varpulis_runtime/
simulator.rs

1//! HVAC Building Simulator for demo purposes
2
3use chrono::Utc;
4use rand::rngs::StdRng;
5use rand::{Rng, SeedableRng};
6use tokio::sync::mpsc;
7use tokio::time;
8
9use crate::event::{Event, HVACStatus, HumidityReading, TemperatureReading};
10
11/// Configuration for the HVAC simulator
12#[derive(Debug, Clone)]
13pub struct SimulatorConfig {
14    pub zones: Vec<ZoneConfig>,
15    pub hvac_units: Vec<HVACConfig>,
16    pub events_per_second: u32,
17    pub anomaly_probability: f64,
18    pub degradation_enabled: bool,
19}
20
21#[derive(Debug, Clone)]
22pub struct ZoneConfig {
23    pub id: String,
24    pub name: String,
25    pub target_temp: f64,
26    pub target_humidity: f64,
27    pub temp_variance: f64,
28    pub humidity_variance: f64,
29}
30
31#[derive(Debug, Clone)]
32pub struct HVACConfig {
33    pub id: String,
34    pub base_power: f64,
35    pub base_pressure: f64,
36}
37
38impl Default for SimulatorConfig {
39    fn default() -> Self {
40        Self {
41            zones: vec![
42                ZoneConfig {
43                    id: "zone_a".to_string(),
44                    name: "Bureaux".to_string(),
45                    target_temp: 22.0,
46                    target_humidity: 50.0,
47                    temp_variance: 1.0,
48                    humidity_variance: 5.0,
49                },
50                ZoneConfig {
51                    id: "zone_b".to_string(),
52                    name: "Salle Serveurs".to_string(),
53                    target_temp: 19.0,
54                    target_humidity: 50.0,
55                    temp_variance: 0.5,
56                    humidity_variance: 3.0,
57                },
58                ZoneConfig {
59                    id: "zone_c".to_string(),
60                    name: "Accueil".to_string(),
61                    target_temp: 21.0,
62                    target_humidity: 50.0,
63                    temp_variance: 2.0,
64                    humidity_variance: 8.0,
65                },
66            ],
67            hvac_units: vec![HVACConfig {
68                id: "cta_main".to_string(),
69                base_power: 15.0,
70                base_pressure: 8.5,
71            }],
72            events_per_second: 10,
73            anomaly_probability: 0.01,
74            degradation_enabled: false,
75        }
76    }
77}
78
79/// HVAC Building Simulator
80#[derive(Debug)]
81pub struct Simulator {
82    config: SimulatorConfig,
83    sender: mpsc::Sender<Event>,
84    tick_count: u64,
85    degradation_factor: f64,
86    rng: StdRng,
87}
88
89impl Simulator {
90    pub fn new(config: SimulatorConfig, sender: mpsc::Sender<Event>) -> Self {
91        Self {
92            config,
93            sender,
94            tick_count: 0,
95            degradation_factor: 1.0,
96            rng: StdRng::from_os_rng(),
97        }
98    }
99
100    /// Run the simulator
101    pub async fn run(&mut self) {
102        let interval_ms = 1000 / self.config.events_per_second as u64;
103        let mut interval = time::interval(time::Duration::from_millis(interval_ms));
104
105        loop {
106            interval.tick().await;
107            self.tick_count += 1;
108
109            // Generate events
110            if let Err(e) = self.generate_events().await {
111                tracing::error!("Failed to send event: {}", e);
112                break;
113            }
114
115            // Update degradation if enabled
116            if self.config.degradation_enabled {
117                self.degradation_factor += 0.0001;
118            }
119        }
120    }
121
122    async fn generate_events(&mut self) -> Result<(), mpsc::error::SendError<Event>> {
123        let rng = &mut self.rng;
124        let now = Utc::now();
125
126        // Generate temperature readings for each zone
127        for zone in &self.config.zones {
128            let is_anomaly = rng.random::<f64>() < self.config.anomaly_probability;
129
130            let temp = if is_anomaly {
131                // Anomaly: temperature spike
132                zone.target_temp + rng.random_range(5.0..10.0)
133            } else {
134                zone.target_temp + rng.random_range(-zone.temp_variance..zone.temp_variance)
135            };
136
137            let reading = TemperatureReading {
138                sensor_id: format!("{}_temp_01", zone.id),
139                zone: zone.id.clone(),
140                value: temp,
141                timestamp: now,
142            };
143            self.sender.send(reading.into()).await?;
144
145            // Generate humidity reading (less frequent)
146            if self.tick_count.is_multiple_of(3) {
147                let humidity = zone.target_humidity
148                    + rng.random_range(-zone.humidity_variance..zone.humidity_variance);
149
150                let reading = HumidityReading {
151                    sensor_id: format!("{}_hum_01", zone.id),
152                    zone: zone.id.clone(),
153                    value: humidity,
154                    timestamp: now,
155                };
156                self.sender.send(reading.into()).await?;
157            }
158        }
159
160        // Generate HVAC status (less frequent)
161        if self.tick_count.is_multiple_of(5) {
162            for hvac in &self.config.hvac_units {
163                let power = hvac
164                    .base_power
165                    .mul_add(self.degradation_factor, rng.random_range(-0.5..0.5));
166                let pressure =
167                    hvac.base_pressure / self.degradation_factor + rng.random_range(-0.1..0.1);
168
169                let status = HVACStatus {
170                    unit_id: hvac.id.clone(),
171                    mode: "cooling".to_string(),
172                    power_consumption: power,
173                    fan_speed: 1200 + rng.random_range(-50..50),
174                    compressor_pressure: pressure,
175                    timestamp: now,
176                };
177                self.sender.send(status.into()).await?;
178            }
179        }
180
181        Ok(())
182    }
183}
184
185/// Create a simulator with default config and return sender/receiver
186pub fn create_default_simulator() -> (Simulator, mpsc::Receiver<Event>) {
187    let (tx, rx) = mpsc::channel(1000);
188    let config = SimulatorConfig::default();
189    let simulator = Simulator::new(config, tx);
190    (simulator, rx)
191}
192
193/// Create a simulator that produces anomalies
194pub fn create_anomaly_simulator() -> (Simulator, mpsc::Receiver<Event>) {
195    let (tx, rx) = mpsc::channel(1000);
196    let config = SimulatorConfig {
197        anomaly_probability: 0.1, // 10% anomaly rate
198        ..Default::default()
199    };
200    let simulator = Simulator::new(config, tx);
201    (simulator, rx)
202}
203
204/// Create a simulator with degradation
205pub fn create_degradation_simulator() -> (Simulator, mpsc::Receiver<Event>) {
206    let (tx, rx) = mpsc::channel(1000);
207    let config = SimulatorConfig {
208        degradation_enabled: true,
209        ..Default::default()
210    };
211    let simulator = Simulator::new(config, tx);
212    (simulator, rx)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    // ==========================================================================
220    // Config Tests
221    // ==========================================================================
222
223    #[test]
224    fn test_simulator_config_default() {
225        let config = SimulatorConfig::default();
226        assert_eq!(config.zones.len(), 3);
227        assert_eq!(config.hvac_units.len(), 1);
228        assert_eq!(config.events_per_second, 10);
229        assert!((config.anomaly_probability - 0.01).abs() < 0.001);
230        assert!(!config.degradation_enabled);
231    }
232
233    #[test]
234    fn test_zone_config() {
235        let config = SimulatorConfig::default();
236        let zone_a = &config.zones[0];
237        assert_eq!(zone_a.id, "zone_a");
238        assert_eq!(zone_a.name, "Bureaux");
239        assert!((zone_a.target_temp - 22.0).abs() < 0.01);
240    }
241
242    #[test]
243    fn test_hvac_config() {
244        let config = SimulatorConfig::default();
245        let hvac = &config.hvac_units[0];
246        assert_eq!(hvac.id, "cta_main");
247        assert!((hvac.base_power - 15.0).abs() < 0.01);
248        assert!((hvac.base_pressure - 8.5).abs() < 0.01);
249    }
250
251    // ==========================================================================
252    // Simulator Creation Tests
253    // ==========================================================================
254
255    #[test]
256    fn test_create_default_simulator() {
257        let (sim, _rx) = create_default_simulator();
258        assert_eq!(sim.tick_count, 0);
259        assert!((sim.degradation_factor - 1.0).abs() < 0.001);
260    }
261
262    #[test]
263    fn test_create_anomaly_simulator() {
264        let (sim, _rx) = create_anomaly_simulator();
265        assert!((sim.config.anomaly_probability - 0.1).abs() < 0.001);
266    }
267
268    #[test]
269    fn test_create_degradation_simulator() {
270        let (sim, _rx) = create_degradation_simulator();
271        assert!(sim.config.degradation_enabled);
272    }
273
274    #[test]
275    fn test_simulator_new() {
276        let (tx, _rx) = mpsc::channel(100);
277        let config = SimulatorConfig::default();
278        let sim = Simulator::new(config, tx);
279        assert_eq!(sim.tick_count, 0);
280    }
281
282    // ==========================================================================
283    // Event Generation Tests
284    // ==========================================================================
285
286    #[tokio::test]
287    async fn test_simulator_generate_events() {
288        let (tx, mut rx) = mpsc::channel(100);
289        let config = SimulatorConfig::default();
290        let mut sim = Simulator::new(config, tx);
291
292        // Generate one batch of events
293        sim.generate_events().await.unwrap();
294
295        // Should have temperature readings for each zone
296        let mut temp_count = 0;
297        while let Ok(event) = rx.try_recv() {
298            if &*event.event_type == "TemperatureReading" {
299                temp_count += 1;
300            }
301        }
302        assert_eq!(temp_count, 3); // One per zone
303    }
304
305    #[tokio::test]
306    async fn test_simulator_humidity_generation() {
307        let (tx, mut rx) = mpsc::channel(100);
308        let config = SimulatorConfig::default();
309        let mut sim = Simulator::new(config, tx);
310
311        // Generate events at tick 3 (humidity is generated when tick % 3 == 0)
312        sim.tick_count = 2; // Will become 3 in generate_events after increment
313        sim.generate_events().await.unwrap();
314        sim.tick_count = 3;
315        sim.generate_events().await.unwrap();
316
317        let mut humidity_count = 0;
318        while let Ok(event) = rx.try_recv() {
319            if &*event.event_type == "HumidityReading" {
320                humidity_count += 1;
321            }
322        }
323        assert!(humidity_count >= 3); // Should have humidity readings
324    }
325
326    #[tokio::test]
327    async fn test_simulator_hvac_generation() {
328        let (tx, mut rx) = mpsc::channel(100);
329        let config = SimulatorConfig::default();
330        let mut sim = Simulator::new(config, tx);
331
332        // Generate events at tick 5 (HVAC is generated when tick % 5 == 0)
333        sim.tick_count = 4;
334        sim.generate_events().await.unwrap();
335        sim.tick_count = 5;
336        sim.generate_events().await.unwrap();
337
338        let mut hvac_count = 0;
339        while let Ok(event) = rx.try_recv() {
340            if &*event.event_type == "HVACStatus" {
341                hvac_count += 1;
342            }
343        }
344        assert!(hvac_count >= 1); // Should have at least one HVAC status
345    }
346
347    #[tokio::test]
348    #[allow(clippy::field_reassign_with_default)]
349    async fn test_simulator_with_degradation() {
350        let (tx, _rx) = mpsc::channel(100);
351        let mut config = SimulatorConfig::default();
352        config.degradation_enabled = true;
353        let mut sim = Simulator::new(config, tx);
354
355        let initial_degradation = sim.degradation_factor;
356
357        // Generate events multiple times
358        for _ in 0..10 {
359            sim.generate_events().await.unwrap();
360            if sim.config.degradation_enabled {
361                sim.degradation_factor += 0.0001;
362            }
363        }
364
365        // Degradation should have increased
366        assert!(sim.degradation_factor > initial_degradation);
367    }
368
369    #[tokio::test]
370    async fn test_simulator_event_fields() {
371        let (tx, mut rx) = mpsc::channel(100);
372        let config = SimulatorConfig::default();
373        let mut sim = Simulator::new(config, tx);
374
375        sim.generate_events().await.unwrap();
376
377        // Check temperature reading has correct fields
378        if let Ok(event) = rx.try_recv() {
379            if &*event.event_type == "TemperatureReading" {
380                assert!(event.get_str("sensor_id").is_some());
381                assert!(event.get_str("zone").is_some());
382                assert!(event.get_float("value").is_some());
383            }
384        }
385    }
386
387    #[tokio::test]
388    async fn test_simulator_channel_closed() {
389        let (tx, rx) = mpsc::channel(1);
390        let config = SimulatorConfig::default();
391        let mut sim = Simulator::new(config, tx);
392
393        // Drop receiver to close channel
394        drop(rx);
395
396        // Generate events should fail
397        let result = sim.generate_events().await;
398        assert!(result.is_err());
399    }
400}