1use mabi_core::tags::Tags;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Scenario {
10 pub name: String,
12
13 #[serde(default)]
15 pub description: String,
16
17 #[serde(default)]
19 pub duration_secs: u64,
20
21 #[serde(default = "default_time_scale")]
23 pub time_scale: f64,
24
25 #[serde(default)]
27 pub devices: Vec<ScenarioDevice>,
28
29 #[serde(default)]
31 pub points: Vec<ScenarioPoint>,
32
33 #[serde(default)]
35 pub events: Vec<ScenarioEvent>,
36
37 #[serde(default)]
39 pub variables: HashMap<String, f64>,
40}
41
42fn default_time_scale() -> f64 {
43 1.0
44}
45
46impl Default for Scenario {
47 fn default() -> Self {
48 Self {
49 name: "Unnamed Scenario".to_string(),
50 description: String::new(),
51 duration_secs: 0,
52 time_scale: 1.0,
53 devices: Vec::new(),
54 points: Vec::new(),
55 events: Vec::new(),
56 variables: HashMap::new(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ScenarioDevice {
64 pub id: String,
66
67 #[serde(default)]
69 pub name: String,
70
71 pub protocol: String,
73
74 #[serde(default)]
76 pub config: HashMap<String, serde_yaml::Value>,
77
78 #[serde(default, skip_serializing_if = "Tags::is_empty")]
80 pub tags: Tags,
81}
82
83impl ScenarioDevice {
84 pub fn new(id: impl Into<String>, protocol: impl Into<String>) -> Self {
86 Self {
87 id: id.into(),
88 name: String::new(),
89 protocol: protocol.into(),
90 config: HashMap::new(),
91 tags: Tags::new(),
92 }
93 }
94
95 pub fn with_name(mut self, name: impl Into<String>) -> Self {
97 self.name = name.into();
98 self
99 }
100
101 pub fn with_tags(mut self, tags: Tags) -> Self {
103 self.tags = tags;
104 self
105 }
106
107 pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
109 self.tags.insert(key.into(), value.into());
110 self
111 }
112
113 pub fn with_label(mut self, label: impl Into<String>) -> Self {
115 self.tags.add_label(label.into());
116 self
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ScenarioPoint {
123 pub id: String,
125
126 #[serde(default)]
128 pub device_id: String,
129
130 pub point_id: String,
132
133 pub pattern: PatternConfig,
135
136 #[serde(default = "default_interval")]
138 pub interval_ms: u64,
139
140 #[serde(default, skip_serializing_if = "Tags::is_empty")]
144 pub device_tags: Tags,
145}
146
147fn default_interval() -> u64 {
148 1000
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(tag = "type", rename_all = "lowercase")]
154pub enum PatternConfig {
155 Constant { value: f64 },
157
158 Sine {
160 amplitude: f64,
161 offset: f64,
162 period_secs: f64,
163 #[serde(default)]
164 phase: f64,
165 },
166
167 Cosine {
169 amplitude: f64,
170 offset: f64,
171 period_secs: f64,
172 #[serde(default)]
173 phase: f64,
174 },
175
176 Ramp {
178 start: f64,
179 end: f64,
180 duration_secs: f64,
181 #[serde(default)]
182 repeat: bool,
183 },
184
185 Step {
187 levels: Vec<f64>,
188 step_duration_secs: f64,
189 },
190
191 Random {
193 min: f64,
194 max: f64,
195 #[serde(default = "default_distribution")]
196 distribution: String,
197 },
198
199 Noise { mean: f64, std_dev: f64 },
201
202 Follow {
204 source: String,
205 #[serde(default)]
206 offset: f64,
207 #[serde(default = "default_gain")]
208 gain: f64,
209 #[serde(default)]
210 delay_ms: u64,
211 },
212
213 Replay {
215 file: String,
216 #[serde(default)]
217 loop_replay: bool,
218 },
219}
220
221fn default_distribution() -> String {
222 "uniform".to_string()
223}
224
225fn default_gain() -> f64 {
226 1.0
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ScenarioEvent {
232 pub name: String,
234
235 pub trigger: EventTrigger,
237
238 pub actions: Vec<EventAction>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(tag = "type", rename_all = "lowercase")]
245pub enum EventTrigger {
246 Time { at_secs: f64 },
248
249 Periodic {
251 interval_secs: f64,
252 #[serde(default)]
253 start_secs: f64,
254 },
255
256 Condition {
258 point: String,
259 operator: String,
260 value: f64,
261 },
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266#[serde(tag = "type", rename_all = "lowercase")]
267pub enum EventAction {
268 SetValue { point: String, value: f64 },
270
271 ChangePattern {
273 point: String,
274 pattern: PatternConfig,
275 },
276
277 Log {
279 message: String,
280 #[serde(default = "default_log_level")]
281 level: String,
282 },
283
284 Pause,
286
287 Stop,
289}
290
291fn default_log_level() -> String {
292 "info".to_string()
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_scenario_deserialization() {
301 let yaml = r#"
302name: Test Scenario
303description: A test scenario
304duration_secs: 3600
305time_scale: 1.0
306points:
307 - id: temp
308 device_id: device-001
309 point_id: temperature
310 pattern:
311 type: sine
312 amplitude: 5.0
313 offset: 22.0
314 period_secs: 3600
315 interval_ms: 1000
316"#;
317
318 let scenario: Scenario = serde_yaml::from_str(yaml).unwrap();
319 assert_eq!(scenario.name, "Test Scenario");
320 assert_eq!(scenario.points.len(), 1);
321 }
322
323 #[test]
324 fn test_scenario_with_devices_and_tags() {
325 let yaml = r#"
326name: Tagged Devices Scenario
327description: A scenario with tagged devices
328duration_secs: 3600
329
330devices:
331 - id: hvac-001
332 name: HVAC Unit 1
333 protocol: modbus
334 config:
335 unit_id: 1
336 port: 5020
337 tags:
338 tags:
339 location: building-a
340 floor: "3"
341 labels:
342 - hvac
343 - critical
344
345 - id: hvac-002
346 name: HVAC Unit 2
347 protocol: modbus
348 config:
349 unit_id: 2
350 tags:
351 tags:
352 location: building-a
353 floor: "4"
354 labels:
355 - hvac
356
357points:
358 - id: temp_all_hvac
359 point_id: temperature
360 pattern:
361 type: sine
362 amplitude: 2.0
363 offset: 22.0
364 period_secs: 3600
365 device_tags:
366 labels:
367 - hvac
368"#;
369
370 let scenario: Scenario = serde_yaml::from_str(yaml).unwrap();
371 assert_eq!(scenario.name, "Tagged Devices Scenario");
372 assert_eq!(scenario.devices.len(), 2);
373
374 let device1 = &scenario.devices[0];
375 assert_eq!(device1.id, "hvac-001");
376 assert_eq!(device1.tags.get("location"), Some("building-a"));
377 assert!(device1.tags.has_label("hvac"));
378 assert!(device1.tags.has_label("critical"));
379
380 let device2 = &scenario.devices[1];
381 assert_eq!(device2.tags.get("floor"), Some("4"));
382 assert!(device2.tags.has_label("hvac"));
383 assert!(!device2.tags.has_label("critical"));
384
385 let point = &scenario.points[0];
387 assert!(point.device_tags.has_label("hvac"));
388 }
389
390 #[test]
391 fn test_scenario_device_builder() {
392 let device = ScenarioDevice::new("sensor-001", "modbus")
393 .with_name("Temperature Sensor")
394 .with_tag("zone", "hvac")
395 .with_tag("floor", "1")
396 .with_label("monitored")
397 .with_label("critical");
398
399 assert_eq!(device.id, "sensor-001");
400 assert_eq!(device.protocol, "modbus");
401 assert_eq!(device.tags.get("zone"), Some("hvac"));
402 assert!(device.tags.has_label("monitored"));
403 assert!(device.tags.has_label("critical"));
404 }
405}