1use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
4use bacnet_types::enums::{ObjectType, PropertyIdentifier};
5use bacnet_types::error::Error;
6use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags, Time};
7use bacnet_types::MacAddr;
8use std::borrow::Cow;
9
10use crate::common::{self, read_common_properties};
11use crate::database::ObjectDatabase;
12use crate::event::EventTransition;
13use crate::traits::BACnetObject;
14
15pub struct NotificationClass {
21 oid: ObjectIdentifier,
22 name: String,
23 description: String,
24 status_flags: StatusFlags,
25 out_of_service: bool,
26 reliability: u32,
27 pub notification_class: u32,
29 pub priority: [u8; 3],
31 pub ack_required: [bool; 3],
33 pub recipient_list: Vec<BACnetDestination>,
35}
36
37impl NotificationClass {
38 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
42 let oid = ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, instance)?;
43 Ok(Self {
44 oid,
45 name: name.into(),
46 description: String::new(),
47 status_flags: StatusFlags::empty(),
48 out_of_service: false,
49 reliability: 0,
50 notification_class: instance,
51 priority: [255, 255, 255],
52 ack_required: [false, false, false],
53 recipient_list: Vec::new(),
54 })
55 }
56
57 pub fn set_description(&mut self, desc: impl Into<String>) {
59 self.description = desc.into();
60 }
61
62 pub fn add_destination(&mut self, dest: BACnetDestination) {
64 self.recipient_list.push(dest);
65 }
66}
67
68impl BACnetObject for NotificationClass {
69 fn object_identifier(&self) -> ObjectIdentifier {
70 self.oid
71 }
72
73 fn object_name(&self) -> &str {
74 &self.name
75 }
76
77 fn read_property(
78 &self,
79 property: PropertyIdentifier,
80 array_index: Option<u32>,
81 ) -> Result<PropertyValue, Error> {
82 if let Some(result) = read_common_properties!(self, property, array_index) {
83 return result;
84 }
85 match property {
86 p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
87 ObjectType::NOTIFICATION_CLASS.to_raw(),
88 )),
89 p if p == PropertyIdentifier::EVENT_STATE => {
90 Ok(PropertyValue::Enumerated(0)) }
92 p if p == PropertyIdentifier::NOTIFICATION_CLASS => {
93 Ok(PropertyValue::Unsigned(self.notification_class as u64))
94 }
95 p if p == PropertyIdentifier::PRIORITY => match array_index {
96 Some(0) => Ok(PropertyValue::Unsigned(3)),
97 Some(idx) if (1..=3).contains(&idx) => Ok(PropertyValue::Unsigned(
98 self.priority[(idx - 1) as usize] as u64,
99 )),
100 None => Ok(PropertyValue::List(vec![
101 PropertyValue::Unsigned(self.priority[0] as u64),
102 PropertyValue::Unsigned(self.priority[1] as u64),
103 PropertyValue::Unsigned(self.priority[2] as u64),
104 ])),
105 _ => Err(common::invalid_array_index_error()),
106 },
107 p if p == PropertyIdentifier::ACK_REQUIRED => {
108 let mut byte: u8 = 0;
110 if self.ack_required[0] {
111 byte |= 0x80;
112 } if self.ack_required[1] {
114 byte |= 0x40;
115 } if self.ack_required[2] {
117 byte |= 0x20;
118 } Ok(PropertyValue::BitString {
120 unused_bits: 5,
121 data: vec![byte],
122 })
123 }
124 p if p == PropertyIdentifier::RECIPIENT_LIST => Ok(PropertyValue::List(
125 self.recipient_list
126 .iter()
127 .map(|dest| {
128 PropertyValue::List(vec![
129 PropertyValue::BitString {
131 unused_bits: 1,
132 data: vec![dest.valid_days << 1],
133 },
134 PropertyValue::Time(dest.from_time),
135 PropertyValue::Time(dest.to_time),
136 match &dest.recipient {
138 BACnetRecipient::Device(oid) => {
139 PropertyValue::ObjectIdentifier(*oid)
140 }
141 BACnetRecipient::Address(addr) => {
142 PropertyValue::OctetString(addr.mac_address.to_vec())
143 }
144 },
145 PropertyValue::Unsigned(dest.process_identifier as u64),
146 PropertyValue::Boolean(dest.issue_confirmed_notifications),
147 PropertyValue::BitString {
149 unused_bits: 5,
150 data: vec![dest.transitions << 5],
151 },
152 ])
153 })
154 .collect(),
155 )),
156 _ => Err(common::unknown_property_error()),
157 }
158 }
159
160 fn write_property(
161 &mut self,
162 property: PropertyIdentifier,
163 _array_index: Option<u32>,
164 value: PropertyValue,
165 _priority: Option<u8>,
166 ) -> Result<(), Error> {
167 if property == PropertyIdentifier::NOTIFICATION_CLASS {
168 if let PropertyValue::Unsigned(v) = value {
169 self.notification_class = common::u64_to_u32(v)?;
170 return Ok(());
171 }
172 return Err(common::invalid_data_type_error());
173 }
174 if property == PropertyIdentifier::RECIPIENT_LIST {
175 if let PropertyValue::List(entries) = value {
176 let mut new_list = Vec::with_capacity(entries.len());
177 for entry in entries {
178 if let PropertyValue::List(fields) = entry {
179 if fields.len() < 7 {
180 return Err(common::invalid_data_type_error());
181 }
182 let valid_days = match &fields[0] {
184 PropertyValue::BitString { data, .. } if !data.is_empty() => {
185 data[0] >> 1
186 }
187 _ => return Err(common::invalid_data_type_error()),
188 };
189 let from_time = match fields[1] {
191 PropertyValue::Time(t) => t,
192 _ => return Err(common::invalid_data_type_error()),
193 };
194 let to_time = match fields[2] {
196 PropertyValue::Time(t) => t,
197 _ => return Err(common::invalid_data_type_error()),
198 };
199 let recipient = match &fields[3] {
201 PropertyValue::ObjectIdentifier(oid) => BACnetRecipient::Device(*oid),
202 PropertyValue::OctetString(mac) => {
203 BACnetRecipient::Address(BACnetAddress {
204 network_number: 0,
205 mac_address: MacAddr::from_slice(mac),
206 })
207 }
208 _ => return Err(common::invalid_data_type_error()),
209 };
210 let process_identifier = match fields[4] {
212 PropertyValue::Unsigned(v) => common::u64_to_u32(v)?,
213 _ => return Err(common::invalid_data_type_error()),
214 };
215 let issue_confirmed_notifications = match fields[5] {
217 PropertyValue::Boolean(b) => b,
218 _ => return Err(common::invalid_data_type_error()),
219 };
220 let transitions = match &fields[6] {
222 PropertyValue::BitString { data, .. } if !data.is_empty() => {
223 data[0] >> 5
224 }
225 _ => return Err(common::invalid_data_type_error()),
226 };
227 new_list.push(BACnetDestination {
228 valid_days,
229 from_time,
230 to_time,
231 recipient,
232 process_identifier,
233 issue_confirmed_notifications,
234 transitions,
235 });
236 } else {
237 return Err(common::invalid_data_type_error());
238 }
239 }
240 self.recipient_list = new_list;
241 return Ok(());
242 }
243 return Err(common::invalid_data_type_error());
244 }
245 if let Some(result) =
246 common::write_out_of_service(&mut self.out_of_service, property, &value)
247 {
248 return result;
249 }
250 if let Some(result) = common::write_description(&mut self.description, property, &value) {
251 return result;
252 }
253 Err(common::write_access_denied_error())
254 }
255
256 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
257 static PROPS: &[PropertyIdentifier] = &[
258 PropertyIdentifier::OBJECT_IDENTIFIER,
259 PropertyIdentifier::OBJECT_NAME,
260 PropertyIdentifier::DESCRIPTION,
261 PropertyIdentifier::OBJECT_TYPE,
262 PropertyIdentifier::STATUS_FLAGS,
263 PropertyIdentifier::EVENT_STATE,
264 PropertyIdentifier::OUT_OF_SERVICE,
265 PropertyIdentifier::RELIABILITY,
266 PropertyIdentifier::NOTIFICATION_CLASS,
267 PropertyIdentifier::PRIORITY,
268 PropertyIdentifier::ACK_REQUIRED,
269 PropertyIdentifier::RECIPIENT_LIST,
270 ];
271 Cow::Borrowed(PROPS)
272 }
273}
274
275fn time_to_centiseconds(t: &Time) -> u32 {
277 let h = if t.hour == Time::UNSPECIFIED {
278 0
279 } else {
280 t.hour as u32
281 };
282 let m = if t.minute == Time::UNSPECIFIED {
283 0
284 } else {
285 t.minute as u32
286 };
287 let s = if t.second == Time::UNSPECIFIED {
288 0
289 } else {
290 t.second as u32
291 };
292 let cs = if t.hundredths == Time::UNSPECIFIED {
293 0
294 } else {
295 t.hundredths as u32
296 };
297 h * 360_000 + m * 6_000 + s * 100 + cs
298}
299
300fn time_in_window(current: &Time, from: &Time, to: &Time) -> bool {
304 if from.hour == Time::UNSPECIFIED || to.hour == Time::UNSPECIFIED {
305 return true;
306 }
307 let cur = time_to_centiseconds(current);
308 let from_cs = time_to_centiseconds(from);
309 let to_cs = time_to_centiseconds(to);
310 cur >= from_cs && cur <= to_cs
311}
312
313pub fn get_notification_recipients(
329 db: &ObjectDatabase,
330 notification_class: u32,
331 transition: EventTransition,
332 today_bit: u8,
333 current_time: &Time,
334) -> Vec<(BACnetRecipient, u32, bool)> {
335 let recipient_list_val = if let Ok(nc_oid) =
337 ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, notification_class)
338 {
339 if let Some(obj) = db.get(&nc_oid) {
340 match obj.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None) {
341 Ok(PropertyValue::Unsigned(n)) if n as u32 == notification_class => obj
342 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
343 .ok(),
344 _ => None,
345 }
346 } else {
347 None
348 }
349 } else {
350 None
351 };
352
353 let recipient_list_val = recipient_list_val.or_else(|| {
355 db.find_by_type(ObjectType::NOTIFICATION_CLASS)
356 .iter()
357 .find_map(|oid| {
358 let obj = db.get(oid)?;
359 match obj.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None) {
360 Ok(PropertyValue::Unsigned(n)) if n as u32 == notification_class => obj
361 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
362 .ok(),
363 _ => None,
364 }
365 })
366 });
367
368 let recipient_list_val = match recipient_list_val {
369 Some(v) => v,
370 None => return Vec::new(),
371 };
372
373 filter_recipient_list(&recipient_list_val, transition, today_bit, current_time)
374}
375
376pub fn filter_recipient_list(
381 recipient_list_value: &PropertyValue,
382 transition: EventTransition,
383 today_bit: u8,
384 current_time: &Time,
385) -> Vec<(BACnetRecipient, u32, bool)> {
386 let entries = match recipient_list_value {
387 PropertyValue::List(l) => l,
388 _ => return Vec::new(),
389 };
390
391 let transition_mask = transition.bit_mask();
392 let mut result = Vec::new();
393
394 for entry in entries {
395 let fields = match entry {
396 PropertyValue::List(f) if f.len() >= 7 => f,
397 _ => continue,
398 };
399
400 let valid_days = match &fields[0] {
402 PropertyValue::BitString { data, .. } if !data.is_empty() => data[0] >> 1,
403 _ => continue,
404 };
405 if valid_days & today_bit == 0 {
406 continue;
407 }
408
409 let from_time = match &fields[1] {
411 PropertyValue::Time(t) => t,
412 _ => continue,
413 };
414 let to_time = match &fields[2] {
415 PropertyValue::Time(t) => t,
416 _ => continue,
417 };
418 if !time_in_window(current_time, from_time, to_time) {
419 continue;
420 }
421
422 let transitions = match &fields[6] {
424 PropertyValue::BitString { data, .. } if !data.is_empty() => data[0] >> 5,
425 _ => continue,
426 };
427 if transitions & transition_mask == 0 {
428 continue;
429 }
430
431 let recipient = match &fields[3] {
433 PropertyValue::ObjectIdentifier(oid) => BACnetRecipient::Device(*oid),
434 PropertyValue::OctetString(mac) => BACnetRecipient::Address(BACnetAddress {
435 network_number: 0,
436 mac_address: MacAddr::from_slice(mac),
437 }),
438 _ => continue,
439 };
440
441 let process_id = match &fields[4] {
443 PropertyValue::Unsigned(v) => *v as u32,
444 _ => continue,
445 };
446
447 let confirmed = match &fields[5] {
449 PropertyValue::Boolean(b) => *b,
450 _ => continue,
451 };
452
453 result.push((recipient, process_id, confirmed));
454 }
455
456 result
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
463 use bacnet_types::primitives::Time;
464 use bacnet_types::MacAddr;
465
466 fn make_time(hour: u8, minute: u8) -> Time {
467 Time {
468 hour,
469 minute,
470 second: 0,
471 hundredths: 0,
472 }
473 }
474
475 fn make_dest_device(device_instance: u32) -> BACnetDestination {
476 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, device_instance).unwrap();
477 BACnetDestination {
478 valid_days: 0b0111_1111, from_time: make_time(0, 0),
480 to_time: make_time(23, 59),
481 recipient: BACnetRecipient::Device(dev_oid),
482 process_identifier: 1,
483 issue_confirmed_notifications: true,
484 transitions: 0b0000_0111, }
486 }
487
488 #[test]
489 fn object_type_is_notification_class() {
490 let nc = NotificationClass::new(1, "NC-1").unwrap();
491 assert_eq!(
492 nc.object_identifier().object_type(),
493 ObjectType::NOTIFICATION_CLASS
494 );
495 assert_eq!(nc.object_identifier().instance_number(), 1);
496 }
497
498 #[test]
499 fn read_notification_class_number() {
500 let nc = NotificationClass::new(42, "NC-42").unwrap();
501 let val = nc
502 .read_property(PropertyIdentifier::NOTIFICATION_CLASS, None)
503 .unwrap();
504 if let PropertyValue::Unsigned(n) = val {
505 assert_eq!(n, 42);
506 } else {
507 panic!("Expected Unsigned");
508 }
509 }
510
511 #[test]
512 fn read_priority_array_index() {
513 let nc = NotificationClass::new(1, "NC-1").unwrap();
514 let len = nc
516 .read_property(PropertyIdentifier::PRIORITY, Some(0))
517 .unwrap();
518 if let PropertyValue::Unsigned(n) = len {
519 assert_eq!(n, 3);
520 } else {
521 panic!("Expected Unsigned");
522 }
523
524 let p1 = nc
526 .read_property(PropertyIdentifier::PRIORITY, Some(1))
527 .unwrap();
528 if let PropertyValue::Unsigned(n) = p1 {
529 assert_eq!(n, 255);
530 } else {
531 panic!("Expected Unsigned");
532 }
533 }
534
535 #[test]
536 fn read_priority_all() {
537 let nc = NotificationClass::new(1, "NC-1").unwrap();
538 let val = nc
539 .read_property(PropertyIdentifier::PRIORITY, None)
540 .unwrap();
541 if let PropertyValue::List(items) = val {
542 assert_eq!(items.len(), 3);
543 assert_eq!(items[0], PropertyValue::Unsigned(255));
544 assert_eq!(items[1], PropertyValue::Unsigned(255));
545 assert_eq!(items[2], PropertyValue::Unsigned(255));
546 } else {
547 panic!("Expected List");
548 }
549 }
550
551 #[test]
552 fn read_priority_invalid_index() {
553 let nc = NotificationClass::new(1, "NC-1").unwrap();
554 let result = nc.read_property(PropertyIdentifier::PRIORITY, Some(4));
555 assert!(result.is_err());
556 }
557
558 #[test]
559 fn read_object_name() {
560 let nc = NotificationClass::new(1, "NC-1").unwrap();
561 let val = nc
562 .read_property(PropertyIdentifier::OBJECT_NAME, None)
563 .unwrap();
564 if let PropertyValue::CharacterString(s) = val {
565 assert_eq!(s, "NC-1");
566 } else {
567 panic!("Expected CharacterString");
568 }
569 }
570
571 #[test]
572 fn write_notification_class_number() {
573 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
574 nc.write_property(
575 PropertyIdentifier::NOTIFICATION_CLASS,
576 None,
577 PropertyValue::Unsigned(99),
578 None,
579 )
580 .unwrap();
581 assert_eq!(nc.notification_class, 99);
582 }
583
584 #[test]
585 fn write_notification_class_wrong_type() {
586 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
587 let result = nc.write_property(
588 PropertyIdentifier::NOTIFICATION_CLASS,
589 None,
590 PropertyValue::Real(1.0),
591 None,
592 );
593 assert!(result.is_err());
594 }
595
596 #[test]
597 fn property_list_contains_recipient_list() {
598 let nc = NotificationClass::new(1, "NC-1").unwrap();
599 let props = nc.property_list();
600 assert!(props.contains(&PropertyIdentifier::NOTIFICATION_CLASS));
601 assert!(props.contains(&PropertyIdentifier::PRIORITY));
602 assert!(props.contains(&PropertyIdentifier::ACK_REQUIRED));
603 assert!(props.contains(&PropertyIdentifier::RECIPIENT_LIST));
604 }
605
606 #[test]
607 fn read_ack_required_default() {
608 let nc = NotificationClass::new(1, "NC-1").unwrap();
609 let val = nc
610 .read_property(PropertyIdentifier::ACK_REQUIRED, None)
611 .unwrap();
612 if let PropertyValue::BitString { unused_bits, data } = val {
613 assert_eq!(unused_bits, 5);
614 assert_eq!(data, vec![0]); } else {
616 panic!("Expected BitString");
617 }
618 }
619
620 #[test]
621 fn read_recipient_list_empty() {
622 let nc = NotificationClass::new(1, "NC-1").unwrap();
623 let val = nc
624 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
625 .unwrap();
626 if let PropertyValue::List(items) = val {
627 assert!(items.is_empty());
628 } else {
629 panic!("Expected List");
630 }
631 }
632
633 #[test]
634 fn add_destination_device_and_read_back() {
635 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
636 nc.add_destination(make_dest_device(99));
637
638 let val = nc
639 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
640 .unwrap();
641 let PropertyValue::List(outer) = val else {
642 panic!("Expected outer List");
643 };
644 assert_eq!(outer.len(), 1);
645
646 let PropertyValue::List(fields) = &outer[0] else {
647 panic!("Expected inner List");
648 };
649 assert_eq!(fields.len(), 7);
651
652 assert_eq!(
654 fields[0],
655 PropertyValue::BitString {
656 unused_bits: 1,
657 data: vec![0b1111_1110],
658 }
659 );
660
661 assert_eq!(fields[1], PropertyValue::Time(make_time(0, 0)));
663
664 assert_eq!(fields[2], PropertyValue::Time(make_time(23, 59)));
666
667 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 99).unwrap();
669 assert_eq!(fields[3], PropertyValue::ObjectIdentifier(dev_oid));
670
671 assert_eq!(fields[4], PropertyValue::Unsigned(1));
673
674 assert_eq!(fields[5], PropertyValue::Boolean(true));
676
677 assert_eq!(
679 fields[6],
680 PropertyValue::BitString {
681 unused_bits: 5,
682 data: vec![0b1110_0000],
683 }
684 );
685 }
686
687 #[test]
688 fn add_destination_address_variant() {
689 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
690 let mac = MacAddr::from_slice(&[192u8, 168, 1, 100, 0xBA, 0xC0]);
691 let dest = BACnetDestination {
692 valid_days: 0b0011_1110, from_time: make_time(8, 0),
694 to_time: make_time(17, 0),
695 recipient: BACnetRecipient::Address(BACnetAddress {
696 network_number: 0,
697 mac_address: mac.clone(),
698 }),
699 process_identifier: 42,
700 issue_confirmed_notifications: false,
701 transitions: 0b0000_0001, };
703 nc.add_destination(dest);
704
705 let val = nc
706 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
707 .unwrap();
708 let PropertyValue::List(outer) = val else {
709 panic!("Expected outer List");
710 };
711 assert_eq!(outer.len(), 1);
712
713 let PropertyValue::List(fields) = &outer[0] else {
714 panic!("Expected inner List");
715 };
716
717 assert_eq!(fields[3], PropertyValue::OctetString(mac.to_vec()));
719
720 assert_eq!(fields[4], PropertyValue::Unsigned(42));
722
723 assert_eq!(fields[5], PropertyValue::Boolean(false));
725
726 assert_eq!(
728 fields[6],
729 PropertyValue::BitString {
730 unused_bits: 5,
731 data: vec![0b0010_0000],
732 }
733 );
734 }
735
736 #[test]
737 fn add_multiple_destinations() {
738 let mut nc = NotificationClass::new(5, "NC-5").unwrap();
739 nc.add_destination(make_dest_device(100));
740 nc.add_destination(make_dest_device(200));
741 nc.add_destination(make_dest_device(300));
742
743 let val = nc
744 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
745 .unwrap();
746 let PropertyValue::List(outer) = val else {
747 panic!("Expected List");
748 };
749 assert_eq!(outer.len(), 3);
750 }
751
752 #[test]
753 fn write_recipient_list_clears_existing() {
754 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
755 nc.add_destination(make_dest_device(10));
756 nc.add_destination(make_dest_device(20));
757 assert_eq!(nc.recipient_list.len(), 2);
758
759 nc.write_property(
761 PropertyIdentifier::RECIPIENT_LIST,
762 None,
763 PropertyValue::List(vec![]),
764 None,
765 )
766 .unwrap();
767 assert!(nc.recipient_list.is_empty());
768 }
769
770 #[test]
771 fn write_recipient_list_wrong_type_denied() {
772 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
773 let result = nc.write_property(
774 PropertyIdentifier::RECIPIENT_LIST,
775 None,
776 PropertyValue::Unsigned(0),
777 None,
778 );
779 assert!(result.is_err());
780 }
781
782 #[test]
783 fn write_recipient_list_round_trip() {
784 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
785 nc.add_destination(make_dest_device(10));
786 let encoded = nc
788 .read_property(PropertyIdentifier::RECIPIENT_LIST, None)
789 .unwrap();
790 nc.write_property(PropertyIdentifier::RECIPIENT_LIST, None, encoded, None)
791 .unwrap();
792 assert_eq!(nc.recipient_list.len(), 1);
793 assert_eq!(nc.recipient_list[0].process_identifier, 1);
794 }
795
796 #[test]
797 fn read_event_state_default() {
798 let nc = NotificationClass::new(1, "NC-1").unwrap();
799 let val = nc
800 .read_property(PropertyIdentifier::EVENT_STATE, None)
801 .unwrap();
802 assert_eq!(val, PropertyValue::Enumerated(0)); }
804
805 #[test]
806 fn write_out_of_service() {
807 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
808 nc.write_property(
809 PropertyIdentifier::OUT_OF_SERVICE,
810 None,
811 PropertyValue::Boolean(true),
812 None,
813 )
814 .unwrap();
815 let val = nc
816 .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
817 .unwrap();
818 assert_eq!(val, PropertyValue::Boolean(true));
819 }
820
821 #[test]
822 fn write_unknown_property_denied() {
823 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
824 let result = nc.write_property(
825 PropertyIdentifier::PRESENT_VALUE,
826 None,
827 PropertyValue::Real(1.0),
828 None,
829 );
830 assert!(result.is_err());
831 }
832
833 fn make_dest(
838 device_instance: u32,
839 valid_days: u8,
840 from: Time,
841 to: Time,
842 confirmed: bool,
843 transitions: u8,
844 ) -> BACnetDestination {
845 let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, device_instance).unwrap();
846 BACnetDestination {
847 valid_days,
848 from_time: from,
849 to_time: to,
850 recipient: BACnetRecipient::Device(dev_oid),
851 process_identifier: device_instance,
852 issue_confirmed_notifications: confirmed,
853 transitions,
854 }
855 }
856
857 #[test]
858 fn get_recipients_filters_by_transition() {
859 let mut db = ObjectDatabase::new();
860 let mut nc = NotificationClass::new(1, "NC-1").unwrap();
861
862 nc.add_destination(make_dest(
864 10,
865 0b0111_1111,
866 make_time(0, 0),
867 make_time(23, 59),
868 false,
869 0b0000_0001,
870 ));
871 nc.add_destination(make_dest(
873 20,
874 0b0111_1111,
875 make_time(0, 0),
876 make_time(23, 59),
877 true,
878 0b0000_0100,
879 ));
880 nc.add_destination(make_dest(
882 30,
883 0b0111_1111,
884 make_time(0, 0),
885 make_time(23, 59),
886 false,
887 0b0000_0111,
888 ));
889 db.add(Box::new(nc)).unwrap();
890
891 let now = make_time(12, 0);
892 let monday_bit = 0x02; let r = get_notification_recipients(&db, 1, EventTransition::ToOffnormal, monday_bit, &now);
896 assert_eq!(r.len(), 2);
897 assert_eq!(r[0].1, 10); assert_eq!(r[1].1, 30);
899
900 let r = get_notification_recipients(&db, 1, EventTransition::ToNormal, monday_bit, &now);
902 assert_eq!(r.len(), 2);
903 assert_eq!(r[0].1, 20);
904 assert!(r[0].2); assert_eq!(r[1].1, 30);
906
907 let r = get_notification_recipients(&db, 1, EventTransition::ToFault, monday_bit, &now);
909 assert_eq!(r.len(), 1);
910 assert_eq!(r[0].1, 30);
911 }
912
913 #[test]
914 fn get_recipients_filters_by_day() {
915 let mut db = ObjectDatabase::new();
916 let mut nc = NotificationClass::new(2, "NC-2").unwrap();
917
918 nc.add_destination(make_dest(
920 10,
921 0b0011_1110,
922 make_time(0, 0),
923 make_time(23, 59),
924 false,
925 0b0000_0111,
926 ));
927 db.add(Box::new(nc)).unwrap();
928
929 let now = make_time(12, 0);
930
931 let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x02, &now);
933 assert_eq!(r.len(), 1);
934
935 let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x01, &now);
937 assert!(r.is_empty());
938
939 let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x40, &now);
941 assert!(r.is_empty());
942 }
943
944 #[test]
945 fn get_recipients_filters_by_time_window() {
946 let mut db = ObjectDatabase::new();
947 let mut nc = NotificationClass::new(3, "NC-3").unwrap();
948
949 nc.add_destination(make_dest(
951 10,
952 0b0111_1111,
953 make_time(8, 0),
954 make_time(17, 0),
955 false,
956 0b0000_0111,
957 ));
958 db.add(Box::new(nc)).unwrap();
959
960 let monday_bit = 0x02;
961
962 let r = get_notification_recipients(
964 &db,
965 3,
966 EventTransition::ToOffnormal,
967 monday_bit,
968 &make_time(12, 0),
969 );
970 assert_eq!(r.len(), 1);
971
972 let r = get_notification_recipients(
974 &db,
975 3,
976 EventTransition::ToOffnormal,
977 monday_bit,
978 &make_time(7, 0),
979 );
980 assert!(r.is_empty());
981
982 let r = get_notification_recipients(
984 &db,
985 3,
986 EventTransition::ToOffnormal,
987 monday_bit,
988 &make_time(18, 0),
989 );
990 assert!(r.is_empty());
991 }
992
993 #[test]
994 fn get_recipients_returns_empty_for_missing_class() {
995 let db = ObjectDatabase::new();
996 let r = get_notification_recipients(
997 &db,
998 99,
999 EventTransition::ToOffnormal,
1000 0x02,
1001 &make_time(12, 0),
1002 );
1003 assert!(r.is_empty());
1004 }
1005
1006 #[test]
1007 fn get_recipients_returns_empty_for_empty_list() {
1008 let mut db = ObjectDatabase::new();
1009 let nc = NotificationClass::new(1, "NC-1").unwrap();
1010 db.add(Box::new(nc)).unwrap();
1011
1012 let r = get_notification_recipients(
1013 &db,
1014 1,
1015 EventTransition::ToOffnormal,
1016 0x02,
1017 &make_time(12, 0),
1018 );
1019 assert!(r.is_empty());
1020 }
1021
1022 #[test]
1023 fn event_state_change_transition_mapping() {
1024 use crate::event::EventStateChange;
1025 use bacnet_types::enums::EventState;
1026
1027 let to_normal = EventStateChange {
1028 from: EventState::HIGH_LIMIT,
1029 to: EventState::NORMAL,
1030 };
1031 assert_eq!(to_normal.transition(), EventTransition::ToNormal);
1032
1033 let to_fault = EventStateChange {
1034 from: EventState::NORMAL,
1035 to: EventState::FAULT,
1036 };
1037 assert_eq!(to_fault.transition(), EventTransition::ToFault);
1038
1039 let to_high = EventStateChange {
1040 from: EventState::NORMAL,
1041 to: EventState::HIGH_LIMIT,
1042 };
1043 assert_eq!(to_high.transition(), EventTransition::ToOffnormal);
1044
1045 let to_low = EventStateChange {
1046 from: EventState::NORMAL,
1047 to: EventState::LOW_LIMIT,
1048 };
1049 assert_eq!(to_low.transition(), EventTransition::ToOffnormal);
1050 }
1051}