1use bacnet_objects::database::ObjectDatabase;
11use bacnet_objects::event::EventStateChange;
12use bacnet_types::enums::{EventState, EventType, ObjectType, PropertyIdentifier};
13use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct EventEnrollmentTransition {
18 pub enrollment_oid: ObjectIdentifier,
20 pub monitored_oid: ObjectIdentifier,
22 pub change: EventStateChange,
24 pub event_type: EventType,
26}
27
28pub fn encode_out_of_range_params(high_limit: f32, low_limit: f32, deadband: f32) -> Vec<u8> {
32 let mut buf = Vec::with_capacity(12);
33 buf.extend_from_slice(&high_limit.to_le_bytes());
34 buf.extend_from_slice(&low_limit.to_le_bytes());
35 buf.extend_from_slice(&deadband.to_le_bytes());
36 buf
37}
38
39pub fn encode_floating_limit_params(
42 setpoint: f32,
43 high_diff_limit: f32,
44 low_diff_limit: f32,
45 deadband: f32,
46) -> Vec<u8> {
47 let mut buf = Vec::with_capacity(16);
48 buf.extend_from_slice(&setpoint.to_le_bytes());
49 buf.extend_from_slice(&high_diff_limit.to_le_bytes());
50 buf.extend_from_slice(&low_diff_limit.to_le_bytes());
51 buf.extend_from_slice(&deadband.to_le_bytes());
52 buf
53}
54
55pub fn encode_change_of_state_params(alarm_values: &[u32]) -> Vec<u8> {
57 let mut buf = Vec::with_capacity(4 + alarm_values.len() * 4);
58 buf.extend_from_slice(&(alarm_values.len() as u32).to_le_bytes());
59 for &v in alarm_values {
60 buf.extend_from_slice(&v.to_le_bytes());
61 }
62 buf
63}
64
65pub fn encode_change_of_value_params(increment: f32) -> Vec<u8> {
67 increment.to_le_bytes().to_vec()
68}
69
70pub fn encode_change_of_bitstring_params(mask: &[u8], alarm_bits: &[u8]) -> Vec<u8> {
73 let len = mask.len().min(alarm_bits.len());
74 let mut buf = Vec::with_capacity(4 + len * 2);
75 buf.extend_from_slice(&(len as u32).to_le_bytes());
76 buf.extend_from_slice(&mask[..len]);
77 buf.extend_from_slice(&alarm_bits[..len]);
78 buf
79}
80
81fn eval_out_of_range(params: &[u8], value: f32, current: EventState) -> EventState {
87 if params.len() < 12 {
88 return current;
89 }
90 let high_limit = f32::from_le_bytes([params[0], params[1], params[2], params[3]]);
91 let low_limit = f32::from_le_bytes([params[4], params[5], params[6], params[7]]);
92 let deadband = f32::from_le_bytes([params[8], params[9], params[10], params[11]]);
93
94 match current {
95 s if s == EventState::NORMAL => {
96 if value > high_limit {
97 EventState::HIGH_LIMIT
98 } else if value < low_limit {
99 EventState::LOW_LIMIT
100 } else {
101 EventState::NORMAL
102 }
103 }
104 s if s == EventState::HIGH_LIMIT => {
105 if value < low_limit {
106 EventState::LOW_LIMIT
107 } else if value < high_limit - deadband {
108 EventState::NORMAL
109 } else {
110 EventState::HIGH_LIMIT
111 }
112 }
113 s if s == EventState::LOW_LIMIT => {
114 if value > high_limit {
115 EventState::HIGH_LIMIT
116 } else if value > low_limit + deadband {
117 EventState::NORMAL
118 } else {
119 EventState::LOW_LIMIT
120 }
121 }
122 _ => current,
123 }
124}
125
126fn eval_floating_limit(params: &[u8], value: f32, current: EventState) -> EventState {
131 if params.len() < 16 {
132 return current;
133 }
134 let setpoint = f32::from_le_bytes([params[0], params[1], params[2], params[3]]);
135 let high_diff = f32::from_le_bytes([params[4], params[5], params[6], params[7]]);
136 let low_diff = f32::from_le_bytes([params[8], params[9], params[10], params[11]]);
137 let deadband = f32::from_le_bytes([params[12], params[13], params[14], params[15]]);
138
139 let high_limit = setpoint + high_diff;
140 let low_limit = setpoint - low_diff;
141
142 match current {
143 s if s == EventState::NORMAL => {
144 if value > high_limit {
145 EventState::HIGH_LIMIT
146 } else if value < low_limit {
147 EventState::LOW_LIMIT
148 } else {
149 EventState::NORMAL
150 }
151 }
152 s if s == EventState::HIGH_LIMIT => {
153 if value < low_limit {
154 EventState::LOW_LIMIT
155 } else if value < high_limit - deadband {
156 EventState::NORMAL
157 } else {
158 EventState::HIGH_LIMIT
159 }
160 }
161 s if s == EventState::LOW_LIMIT => {
162 if value > high_limit {
163 EventState::HIGH_LIMIT
164 } else if value > low_limit + deadband {
165 EventState::NORMAL
166 } else {
167 EventState::LOW_LIMIT
168 }
169 }
170 _ => current,
171 }
172}
173
174fn eval_change_of_state(params: &[u8], value: u32, _current: EventState) -> EventState {
178 if params.len() < 4 {
179 return EventState::NORMAL;
180 }
181 let count = u32::from_le_bytes([params[0], params[1], params[2], params[3]]) as usize;
182 let needed = 4usize.saturating_add(count.saturating_mul(4));
183 if params.len() < needed {
184 return EventState::NORMAL;
185 }
186 for i in 0..count {
187 let offset = 4 + i * 4;
188 let alarm_val = u32::from_le_bytes([
189 params[offset],
190 params[offset + 1],
191 params[offset + 2],
192 params[offset + 3],
193 ]);
194 if value == alarm_val {
195 return EventState::OFFNORMAL;
196 }
197 }
198 EventState::NORMAL
199}
200
201fn eval_change_of_bitstring(params: &[u8], value_bits: &[u8], _current: EventState) -> EventState {
205 if params.len() < 4 {
206 return EventState::NORMAL;
207 }
208 let mask_len = u32::from_le_bytes([params[0], params[1], params[2], params[3]]) as usize;
209 let needed = 4usize.saturating_add(mask_len.saturating_mul(2));
210 if params.len() < needed {
211 return EventState::NORMAL;
212 }
213
214 let mask = ¶ms[4..4 + mask_len];
215 let alarm_bits = ¶ms[4 + mask_len..4 + 2 * mask_len];
216
217 for i in 0..mask_len {
218 let monitored_byte = value_bits.get(i).copied().unwrap_or(0);
219 if (monitored_byte & mask[i]) != (alarm_bits[i] & mask[i]) {
220 return EventState::NORMAL;
221 }
222 }
223 EventState::OFFNORMAL
224}
225
226fn eval_change_of_value(params: &[u8], value: f32, _current: EventState) -> EventState {
230 if params.len() < 4 {
231 return EventState::NORMAL;
232 }
233 let increment = f32::from_le_bytes([params[0], params[1], params[2], params[3]]);
234 if increment <= 0.0 || !increment.is_finite() {
235 return EventState::NORMAL;
236 }
237 if value.abs() >= increment {
238 EventState::OFFNORMAL
239 } else {
240 EventState::NORMAL
241 }
242}
243
244fn extract_real(pv: &PropertyValue) -> Option<f32> {
246 match pv {
247 PropertyValue::Real(v) => Some(*v),
248 PropertyValue::Double(v) => Some(*v as f32),
249 PropertyValue::Unsigned(v) => Some(*v as f32),
250 PropertyValue::Signed(v) => Some(*v as f32),
251 _ => None,
252 }
253}
254
255fn extract_enumerated(pv: &PropertyValue) -> Option<u32> {
257 match pv {
258 PropertyValue::Enumerated(v) => Some(*v),
259 PropertyValue::Unsigned(v) => Some(*v as u32),
260 _ => None,
261 }
262}
263
264fn extract_bitstring(pv: &PropertyValue) -> Option<Vec<u8>> {
266 match pv {
267 PropertyValue::BitString { data, .. } => Some(data.clone()),
268 _ => None,
269 }
270}
271
272fn read_object_property_ref(
276 enrollment: &dyn bacnet_objects::traits::BACnetObject,
277) -> Option<(ObjectIdentifier, PropertyIdentifier)> {
278 match enrollment.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None) {
279 Ok(PropertyValue::List(ref items)) if items.len() >= 2 => {
280 let obj_id = match &items[0] {
281 PropertyValue::ObjectIdentifier(oid) => *oid,
282 _ => return None,
283 };
284 let prop_id = match &items[1] {
285 PropertyValue::Unsigned(v) => PropertyIdentifier::from_raw(*v as u32),
286 _ => return None,
287 };
288 Some((obj_id, prop_id))
289 }
290 _ => None,
291 }
292}
293
294pub fn evaluate_event_enrollments(db: &mut ObjectDatabase) -> Vec<EventEnrollmentTransition> {
299 let oids = db.find_by_type(ObjectType::EVENT_ENROLLMENT);
300
301 let mut updates: Vec<(
302 ObjectIdentifier,
303 ObjectIdentifier,
304 u32,
305 EventState,
306 EventState,
307 )> = Vec::new();
308
309 for oid in &oids {
310 let Some(enrollment) = db.get(oid) else {
311 continue;
312 };
313
314 if let Ok(PropertyValue::Boolean(true)) =
315 enrollment.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
316 {
317 continue;
318 }
319
320 let event_type_raw = match enrollment.read_property(PropertyIdentifier::EVENT_TYPE, None) {
321 Ok(PropertyValue::Enumerated(v)) => v,
322 _ => continue,
323 };
324
325 let current_state = match enrollment.read_property(PropertyIdentifier::EVENT_STATE, None) {
326 Ok(PropertyValue::Enumerated(v)) => EventState::from_raw(v),
327 _ => continue,
328 };
329
330 let event_enable = match enrollment.read_property(PropertyIdentifier::EVENT_ENABLE, None) {
331 Ok(PropertyValue::BitString { data, .. }) => data.first().map(|b| b >> 5).unwrap_or(0),
332 _ => 0,
333 };
334
335 let params = match enrollment.read_property(PropertyIdentifier::EVENT_PARAMETERS, None) {
336 Ok(PropertyValue::OctetString(bytes)) => bytes,
337 _ => Vec::new(),
338 };
339
340 let Some((monitored_oid, monitored_prop)) = read_object_property_ref(enrollment) else {
341 continue;
342 };
343
344 let Some(monitored_obj) = db.get(&monitored_oid) else {
345 continue;
346 };
347 let monitored_value = match monitored_obj.read_property(monitored_prop, None) {
348 Ok(v) => v,
349 Err(_) => continue,
350 };
351
352 let event_type = EventType::from_raw(event_type_raw);
353 let new_state = if event_type == EventType::OUT_OF_RANGE {
354 let Some(val) = extract_real(&monitored_value) else {
355 continue;
356 };
357 eval_out_of_range(¶ms, val, current_state)
358 } else if event_type == EventType::FLOATING_LIMIT {
359 let Some(val) = extract_real(&monitored_value) else {
360 continue;
361 };
362 eval_floating_limit(¶ms, val, current_state)
363 } else if event_type == EventType::CHANGE_OF_STATE {
364 let Some(val) = extract_enumerated(&monitored_value) else {
365 continue;
366 };
367 eval_change_of_state(¶ms, val, current_state)
368 } else if event_type == EventType::CHANGE_OF_BITSTRING {
369 let Some(bits) = extract_bitstring(&monitored_value) else {
370 continue;
371 };
372 eval_change_of_bitstring(¶ms, &bits, current_state)
373 } else if event_type == EventType::CHANGE_OF_VALUE {
374 let Some(val) = extract_real(&monitored_value) else {
375 continue;
376 };
377 eval_change_of_value(¶ms, val, current_state)
378 } else {
379 continue;
380 };
381
382 if new_state == current_state {
383 continue;
384 }
385
386 let transition_enabled = match new_state {
387 s if s == EventState::NORMAL => event_enable & 0x04 != 0,
388 s if s == EventState::HIGH_LIMIT
389 || s == EventState::LOW_LIMIT
390 || s == EventState::OFFNORMAL =>
391 {
392 event_enable & 0x01 != 0
393 }
394 _ => event_enable & 0x02 != 0,
395 };
396
397 if transition_enabled {
398 updates.push((
399 *oid,
400 monitored_oid,
401 event_type_raw,
402 current_state,
403 new_state,
404 ));
405 }
406 }
407
408 let mut transitions = Vec::new();
409 for (oid, monitored_oid, event_type_raw, from_state, to_state) in updates {
410 if let Some(obj) = db.get_mut(&oid) {
411 if obj
412 .write_property(
413 PropertyIdentifier::EVENT_STATE,
414 None,
415 PropertyValue::Enumerated(to_state.to_raw()),
416 None,
417 )
418 .is_ok()
419 {
420 transitions.push(EventEnrollmentTransition {
421 enrollment_oid: oid,
422 monitored_oid,
423 change: EventStateChange {
424 from: from_state,
425 to: to_state,
426 },
427 event_type: EventType::from_raw(event_type_raw),
428 });
429 }
430 }
431 }
432
433 transitions
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use bacnet_objects::analog::AnalogInputObject;
440 use bacnet_objects::binary::BinaryInputObject;
441 use bacnet_objects::event_enrollment::EventEnrollmentObject;
442 use bacnet_objects::traits::BACnetObject;
443 use bacnet_types::constructed::BACnetDeviceObjectPropertyReference;
444
445 fn setup_out_of_range(
447 present_value: f32,
448 high_limit: f32,
449 low_limit: f32,
450 deadband: f32,
451 ) -> (ObjectDatabase, ObjectIdentifier, ObjectIdentifier) {
452 let mut db = ObjectDatabase::new();
453
454 let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
456 ai.set_present_value(present_value);
457 let ai_oid = ai.object_identifier();
458 db.add(Box::new(ai)).unwrap();
459
460 let mut ee =
462 EventEnrollmentObject::new(1, "EE-OOR", EventType::OUT_OF_RANGE.to_raw()).unwrap();
463 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
464 ai_oid,
465 PropertyIdentifier::PRESENT_VALUE.to_raw(),
466 )));
467 ee.set_event_parameters(encode_out_of_range_params(high_limit, low_limit, deadband));
468 ee.set_event_enable(0x07); let ee_oid = ee.object_identifier();
470 db.add(Box::new(ee)).unwrap();
471
472 (db, ee_oid, ai_oid)
473 }
474
475 fn setup_floating_limit(
477 present_value: f32,
478 setpoint: f32,
479 high_diff: f32,
480 low_diff: f32,
481 deadband: f32,
482 ) -> (ObjectDatabase, ObjectIdentifier, ObjectIdentifier) {
483 let mut db = ObjectDatabase::new();
484
485 let mut ai = AnalogInputObject::new(2, "AI-2", 62).unwrap();
486 ai.set_present_value(present_value);
487 let ai_oid = ai.object_identifier();
488 db.add(Box::new(ai)).unwrap();
489
490 let mut ee =
491 EventEnrollmentObject::new(2, "EE-FL", EventType::FLOATING_LIMIT.to_raw()).unwrap();
492 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
493 ai_oid,
494 PropertyIdentifier::PRESENT_VALUE.to_raw(),
495 )));
496 ee.set_event_parameters(encode_floating_limit_params(
497 setpoint, high_diff, low_diff, deadband,
498 ));
499 ee.set_event_enable(0x07);
500 let ee_oid = ee.object_identifier();
501 db.add(Box::new(ee)).unwrap();
502
503 (db, ee_oid, ai_oid)
504 }
505
506 fn setup_change_of_state(
508 present_value: u32,
509 alarm_values: &[u32],
510 ) -> (ObjectDatabase, ObjectIdentifier, ObjectIdentifier) {
511 let mut db = ObjectDatabase::new();
512
513 let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
514 bi.set_present_value(present_value);
515 let bi_oid = bi.object_identifier();
516 db.add(Box::new(bi)).unwrap();
517
518 let mut ee =
519 EventEnrollmentObject::new(3, "EE-COS", EventType::CHANGE_OF_STATE.to_raw()).unwrap();
520 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
521 bi_oid,
522 PropertyIdentifier::PRESENT_VALUE.to_raw(),
523 )));
524 ee.set_event_parameters(encode_change_of_state_params(alarm_values));
525 ee.set_event_enable(0x07);
526 let ee_oid = ee.object_identifier();
527 db.add(Box::new(ee)).unwrap();
528
529 (db, ee_oid, bi_oid)
530 }
531
532 #[test]
535 fn out_of_range_normal_stays_normal() {
536 let (mut db, _ee_oid, _ai_oid) = setup_out_of_range(50.0, 80.0, 20.0, 2.0);
537 let transitions = evaluate_event_enrollments(&mut db);
538 assert!(transitions.is_empty());
539 }
540
541 #[test]
542 fn out_of_range_normal_to_high_limit() {
543 let (mut db, ee_oid, ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
544 let transitions = evaluate_event_enrollments(&mut db);
545 assert_eq!(transitions.len(), 1);
546 assert_eq!(transitions[0].enrollment_oid, ee_oid);
547 assert_eq!(transitions[0].monitored_oid, ai_oid);
548 assert_eq!(transitions[0].change.from, EventState::NORMAL);
549 assert_eq!(transitions[0].change.to, EventState::HIGH_LIMIT);
550 assert_eq!(transitions[0].event_type, EventType::OUT_OF_RANGE);
551
552 let obj = db.get(&ee_oid).unwrap();
554 assert_eq!(
555 obj.read_property(PropertyIdentifier::EVENT_STATE, None)
556 .unwrap(),
557 PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw())
558 );
559 }
560
561 #[test]
562 fn out_of_range_normal_to_low_limit() {
563 let (mut db, ee_oid, _ai_oid) = setup_out_of_range(15.0, 80.0, 20.0, 2.0);
564 let transitions = evaluate_event_enrollments(&mut db);
565 assert_eq!(transitions.len(), 1);
566 assert_eq!(transitions[0].change.from, EventState::NORMAL);
567 assert_eq!(transitions[0].change.to, EventState::LOW_LIMIT);
568
569 let obj = db.get(&ee_oid).unwrap();
571 assert_eq!(
572 obj.read_property(PropertyIdentifier::EVENT_STATE, None)
573 .unwrap(),
574 PropertyValue::Enumerated(EventState::LOW_LIMIT.to_raw())
575 );
576 }
577
578 #[test]
579 fn out_of_range_high_to_normal_with_deadband() {
580 let (mut db, ee_oid, ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
581 evaluate_event_enrollments(&mut db);
583
584 let ai = db.get_mut(&ai_oid).unwrap();
586 ai.write_property(
587 PropertyIdentifier::OUT_OF_SERVICE,
588 None,
589 PropertyValue::Boolean(true),
590 None,
591 )
592 .unwrap();
593 ai.write_property(
594 PropertyIdentifier::PRESENT_VALUE,
595 None,
596 PropertyValue::Real(79.0),
597 None,
598 )
599 .unwrap();
600
601 let transitions = evaluate_event_enrollments(&mut db);
602 assert!(transitions.is_empty(), "within deadband — no transition");
603
604 let ai = db.get_mut(&ai_oid).unwrap();
606 ai.write_property(
607 PropertyIdentifier::PRESENT_VALUE,
608 None,
609 PropertyValue::Real(77.0),
610 None,
611 )
612 .unwrap();
613
614 let transitions = evaluate_event_enrollments(&mut db);
615 assert_eq!(transitions.len(), 1);
616 assert_eq!(transitions[0].change.from, EventState::HIGH_LIMIT);
617 assert_eq!(transitions[0].change.to, EventState::NORMAL);
618
619 let obj = db.get(&ee_oid).unwrap();
620 assert_eq!(
621 obj.read_property(PropertyIdentifier::EVENT_STATE, None)
622 .unwrap(),
623 PropertyValue::Enumerated(EventState::NORMAL.to_raw())
624 );
625 }
626
627 #[test]
628 fn out_of_range_no_change_when_already_faulted() {
629 let (mut db, _ee_oid, _ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
630 let t1 = evaluate_event_enrollments(&mut db);
631 assert_eq!(t1.len(), 1);
632
633 let t2 = evaluate_event_enrollments(&mut db);
635 assert!(t2.is_empty());
636 }
637
638 #[test]
639 fn out_of_range_event_enable_suppresses_notification() {
640 let mut db = ObjectDatabase::new();
641
642 let mut ai = AnalogInputObject::new(10, "AI-10", 62).unwrap();
643 ai.set_present_value(85.0);
644 let ai_oid = ai.object_identifier();
645 db.add(Box::new(ai)).unwrap();
646
647 let mut ee =
648 EventEnrollmentObject::new(10, "EE-sup", EventType::OUT_OF_RANGE.to_raw()).unwrap();
649 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
650 ai_oid,
651 PropertyIdentifier::PRESENT_VALUE.to_raw(),
652 )));
653 ee.set_event_parameters(encode_out_of_range_params(80.0, 20.0, 2.0));
654 ee.set_event_enable(0x04); let ee_oid = ee.object_identifier();
656 db.add(Box::new(ee)).unwrap();
657
658 let transitions = evaluate_event_enrollments(&mut db);
660 assert!(transitions.is_empty());
661
662 let obj = db.get(&ee_oid).unwrap();
664 assert_eq!(
665 obj.read_property(PropertyIdentifier::EVENT_STATE, None)
666 .unwrap(),
667 PropertyValue::Enumerated(EventState::NORMAL.to_raw())
668 );
669 }
670
671 #[test]
672 fn out_of_range_skips_out_of_service() {
673 let (mut db, ee_oid, _ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
674
675 let obj = db.get_mut(&ee_oid).unwrap();
677 obj.write_property(
678 PropertyIdentifier::OUT_OF_SERVICE,
679 None,
680 PropertyValue::Boolean(true),
681 None,
682 )
683 .unwrap();
684
685 let transitions = evaluate_event_enrollments(&mut db);
686 assert!(transitions.is_empty());
687 }
688
689 #[test]
692 fn floating_limit_normal_stays_normal() {
693 let (mut db, _ee_oid, _ai_oid) = setup_floating_limit(50.0, 50.0, 10.0, 10.0, 2.0);
695 let transitions = evaluate_event_enrollments(&mut db);
696 assert!(transitions.is_empty());
697 }
698
699 #[test]
700 fn floating_limit_to_high() {
701 let (mut db, ee_oid, ai_oid) = setup_floating_limit(65.0, 50.0, 10.0, 10.0, 2.0);
703 let transitions = evaluate_event_enrollments(&mut db);
704 assert_eq!(transitions.len(), 1);
705 assert_eq!(transitions[0].enrollment_oid, ee_oid);
706 assert_eq!(transitions[0].monitored_oid, ai_oid);
707 assert_eq!(transitions[0].change.from, EventState::NORMAL);
708 assert_eq!(transitions[0].change.to, EventState::HIGH_LIMIT);
709 assert_eq!(transitions[0].event_type, EventType::FLOATING_LIMIT);
710 }
711
712 #[test]
713 fn floating_limit_to_low() {
714 let (mut db, _ee_oid, _ai_oid) = setup_floating_limit(35.0, 50.0, 10.0, 10.0, 2.0);
716 let transitions = evaluate_event_enrollments(&mut db);
717 assert_eq!(transitions.len(), 1);
718 assert_eq!(transitions[0].change.to, EventState::LOW_LIMIT);
719 }
720
721 #[test]
722 fn floating_limit_deadband_hysteresis() {
723 let (mut db, _ee_oid, ai_oid) = setup_floating_limit(65.0, 50.0, 10.0, 10.0, 2.0);
725 evaluate_event_enrollments(&mut db);
726
727 let ai = db.get_mut(&ai_oid).unwrap();
729 ai.write_property(
730 PropertyIdentifier::OUT_OF_SERVICE,
731 None,
732 PropertyValue::Boolean(true),
733 None,
734 )
735 .unwrap();
736 ai.write_property(
737 PropertyIdentifier::PRESENT_VALUE,
738 None,
739 PropertyValue::Real(59.0),
740 None,
741 )
742 .unwrap();
743 let transitions = evaluate_event_enrollments(&mut db);
744 assert!(transitions.is_empty());
745
746 let ai = db.get_mut(&ai_oid).unwrap();
748 ai.write_property(
749 PropertyIdentifier::PRESENT_VALUE,
750 None,
751 PropertyValue::Real(57.0),
752 None,
753 )
754 .unwrap();
755 let transitions = evaluate_event_enrollments(&mut db);
756 assert_eq!(transitions.len(), 1);
757 assert_eq!(transitions[0].change.to, EventState::NORMAL);
758 }
759
760 #[test]
763 fn change_of_state_normal_when_not_in_alarm_set() {
764 let (mut db, _ee_oid, _bi_oid) = setup_change_of_state(0, &[1]);
766 let transitions = evaluate_event_enrollments(&mut db);
767 assert!(transitions.is_empty());
768 }
769
770 #[test]
771 fn change_of_state_to_offnormal() {
772 let (mut db, ee_oid, bi_oid) = setup_change_of_state(1, &[1]);
774 let transitions = evaluate_event_enrollments(&mut db);
775 assert_eq!(transitions.len(), 1);
776 assert_eq!(transitions[0].enrollment_oid, ee_oid);
777 assert_eq!(transitions[0].monitored_oid, bi_oid);
778 assert_eq!(transitions[0].change.from, EventState::NORMAL);
779 assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
780 assert_eq!(transitions[0].event_type, EventType::CHANGE_OF_STATE);
781 }
782
783 #[test]
784 fn change_of_state_back_to_normal() {
785 let (mut db, _ee_oid, bi_oid) = setup_change_of_state(1, &[1]);
786 evaluate_event_enrollments(&mut db);
787
788 let bi = db.get_mut(&bi_oid).unwrap();
790 bi.write_property(
791 PropertyIdentifier::OUT_OF_SERVICE,
792 None,
793 PropertyValue::Boolean(true),
794 None,
795 )
796 .unwrap();
797 bi.write_property(
798 PropertyIdentifier::PRESENT_VALUE,
799 None,
800 PropertyValue::Enumerated(0),
801 None,
802 )
803 .unwrap();
804
805 let transitions = evaluate_event_enrollments(&mut db);
806 assert_eq!(transitions.len(), 1);
807 assert_eq!(transitions[0].change.from, EventState::OFFNORMAL);
808 assert_eq!(transitions[0].change.to, EventState::NORMAL);
809 }
810
811 #[test]
812 fn change_of_state_multiple_alarm_values() {
813 let (mut db, _ee_oid, _bi_oid) = setup_change_of_state(3, &[1, 3, 5]);
815 let transitions = evaluate_event_enrollments(&mut db);
816 assert_eq!(transitions.len(), 1);
817 assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
818 }
819
820 #[test]
823 fn change_of_bitstring_normal() {
824 let mut db = ObjectDatabase::new();
825
826 let mut target =
829 EventEnrollmentObject::new(50, "Target", EventType::NONE.to_raw()).unwrap();
830 target.set_event_enable(0x05); let target_oid = target.object_identifier();
833 db.add(Box::new(target)).unwrap();
834
835 let mut ee =
836 EventEnrollmentObject::new(51, "EE-COBS", EventType::CHANGE_OF_BITSTRING.to_raw())
837 .unwrap();
838 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
839 target_oid,
840 PropertyIdentifier::EVENT_ENABLE.to_raw(),
841 )));
842 ee.set_event_parameters(encode_change_of_bitstring_params(&[0xFF], &[0xE0]));
844 ee.set_event_enable(0x07);
845 db.add(Box::new(ee)).unwrap();
846
847 let transitions = evaluate_event_enrollments(&mut db);
848 assert!(transitions.is_empty());
850 }
851
852 #[test]
853 fn change_of_bitstring_offnormal() {
854 let mut db = ObjectDatabase::new();
855
856 let mut target =
857 EventEnrollmentObject::new(60, "Target2", EventType::NONE.to_raw()).unwrap();
858 target.set_event_enable(0x07); let target_oid = target.object_identifier();
860 db.add(Box::new(target)).unwrap();
861
862 let mut ee =
863 EventEnrollmentObject::new(61, "EE-COBS2", EventType::CHANGE_OF_BITSTRING.to_raw())
864 .unwrap();
865 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
866 target_oid,
867 PropertyIdentifier::EVENT_ENABLE.to_raw(),
868 )));
869 ee.set_event_parameters(encode_change_of_bitstring_params(&[0xE0], &[0xE0]));
871 ee.set_event_enable(0x07);
872 db.add(Box::new(ee)).unwrap();
873
874 let transitions = evaluate_event_enrollments(&mut db);
875 assert_eq!(transitions.len(), 1);
877 assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
878 }
879
880 #[test]
883 fn change_of_value_within_increment() {
884 let mut db = ObjectDatabase::new();
885
886 let mut ai = AnalogInputObject::new(70, "AI-COV", 62).unwrap();
887 ai.set_present_value(3.0);
888 let ai_oid = ai.object_identifier();
889 db.add(Box::new(ai)).unwrap();
890
891 let mut ee =
892 EventEnrollmentObject::new(70, "EE-COV", EventType::CHANGE_OF_VALUE.to_raw()).unwrap();
893 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
894 ai_oid,
895 PropertyIdentifier::PRESENT_VALUE.to_raw(),
896 )));
897 ee.set_event_parameters(encode_change_of_value_params(5.0));
898 ee.set_event_enable(0x07);
899 db.add(Box::new(ee)).unwrap();
900
901 let transitions = evaluate_event_enrollments(&mut db);
903 assert!(transitions.is_empty());
904 }
905
906 #[test]
907 fn change_of_value_exceeds_increment() {
908 let mut db = ObjectDatabase::new();
909
910 let mut ai = AnalogInputObject::new(71, "AI-COV2", 62).unwrap();
911 ai.set_present_value(10.0);
912 let ai_oid = ai.object_identifier();
913 db.add(Box::new(ai)).unwrap();
914
915 let mut ee =
916 EventEnrollmentObject::new(71, "EE-COV2", EventType::CHANGE_OF_VALUE.to_raw()).unwrap();
917 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
918 ai_oid,
919 PropertyIdentifier::PRESENT_VALUE.to_raw(),
920 )));
921 ee.set_event_parameters(encode_change_of_value_params(5.0));
922 ee.set_event_enable(0x07);
923 db.add(Box::new(ee)).unwrap();
924
925 let transitions = evaluate_event_enrollments(&mut db);
927 assert_eq!(transitions.len(), 1);
928 assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
929 }
930
931 #[test]
934 fn evaluates_multiple_enrollments() {
935 let mut db = ObjectDatabase::new();
936
937 let mut ai1 = AnalogInputObject::new(80, "AI-80", 62).unwrap();
939 ai1.set_present_value(90.0); let ai1_oid = ai1.object_identifier();
941 db.add(Box::new(ai1)).unwrap();
942
943 let mut ai2 = AnalogInputObject::new(81, "AI-81", 62).unwrap();
944 ai2.set_present_value(50.0); let ai2_oid = ai2.object_identifier();
946 db.add(Box::new(ai2)).unwrap();
947
948 let mut ee1 =
950 EventEnrollmentObject::new(80, "EE-80", EventType::OUT_OF_RANGE.to_raw()).unwrap();
951 ee1.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
952 ai1_oid,
953 PropertyIdentifier::PRESENT_VALUE.to_raw(),
954 )));
955 ee1.set_event_parameters(encode_out_of_range_params(80.0, 20.0, 2.0));
956 ee1.set_event_enable(0x07);
957 db.add(Box::new(ee1)).unwrap();
958
959 let mut ee2 =
960 EventEnrollmentObject::new(81, "EE-81", EventType::OUT_OF_RANGE.to_raw()).unwrap();
961 ee2.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
962 ai2_oid,
963 PropertyIdentifier::PRESENT_VALUE.to_raw(),
964 )));
965 ee2.set_event_parameters(encode_out_of_range_params(80.0, 20.0, 2.0));
966 ee2.set_event_enable(0x07);
967 db.add(Box::new(ee2)).unwrap();
968
969 let transitions = evaluate_event_enrollments(&mut db);
970 assert_eq!(transitions.len(), 1);
972 assert_eq!(transitions[0].monitored_oid, ai1_oid);
973 }
974
975 #[test]
976 fn missing_monitored_object_is_skipped() {
977 let mut db = ObjectDatabase::new();
978
979 let fake_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 999).unwrap();
980 let mut ee =
981 EventEnrollmentObject::new(90, "EE-miss", EventType::OUT_OF_RANGE.to_raw()).unwrap();
982 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
983 fake_oid,
984 PropertyIdentifier::PRESENT_VALUE.to_raw(),
985 )));
986 ee.set_event_parameters(encode_out_of_range_params(80.0, 20.0, 2.0));
987 ee.set_event_enable(0x07);
988 db.add(Box::new(ee)).unwrap();
989
990 let transitions = evaluate_event_enrollments(&mut db);
992 assert!(transitions.is_empty());
993 }
994
995 #[test]
996 fn no_reference_is_skipped() {
997 let mut db = ObjectDatabase::new();
998
999 let ee =
1000 EventEnrollmentObject::new(91, "EE-noref", EventType::OUT_OF_RANGE.to_raw()).unwrap();
1001 db.add(Box::new(ee)).unwrap();
1002
1003 let transitions = evaluate_event_enrollments(&mut db);
1004 assert!(transitions.is_empty());
1005 }
1006
1007 #[test]
1008 fn empty_parameters_is_skipped() {
1009 let mut db = ObjectDatabase::new();
1010
1011 let mut ai = AnalogInputObject::new(92, "AI-92", 62).unwrap();
1012 ai.set_present_value(100.0);
1013 let ai_oid = ai.object_identifier();
1014 db.add(Box::new(ai)).unwrap();
1015
1016 let mut ee =
1017 EventEnrollmentObject::new(92, "EE-noparam", EventType::OUT_OF_RANGE.to_raw()).unwrap();
1018 ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
1019 ai_oid,
1020 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1021 )));
1022 ee.set_event_enable(0x07);
1024 db.add(Box::new(ee)).unwrap();
1025
1026 let transitions = evaluate_event_enrollments(&mut db);
1027 assert!(transitions.is_empty());
1028 }
1029}