1use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
25use bacnet_types::enums::{ObjectType, PropertyIdentifier};
26use bacnet_types::error::Error;
27use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags, Time};
28use bacnet_types::MacAddr;
29use std::borrow::Cow;
30
31use crate::common::{self, read_common_properties};
32use crate::database::ObjectDatabase;
33use crate::event::EventTransition;
34use crate::traits::BACnetObject;
35
36mod enrollment_summary;
37#[doc(hidden)]
38pub use enrollment_summary::{
39 resolve_enrollment_summary_class_internal, EnrollmentSummaryClassProjection,
40 EnrollmentSummaryClassProjectionError,
41};
42
43pub struct NotificationClass {
49 oid: ObjectIdentifier,
50 name: String,
51 description: String,
52 status_flags: StatusFlags,
53 out_of_service: bool,
54 reliability: u32,
55 pub notification_class: u32,
57 pub priority: [u8; 3],
59 pub ack_required: [bool; 3],
61 pub recipient_list: Vec<BACnetDestination>,
63}
64
65impl NotificationClass {
66 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
70 let oid = ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, instance)?;
71 Ok(Self {
72 oid,
73 name: name.into(),
74 description: String::new(),
75 status_flags: StatusFlags::empty(),
76 out_of_service: false,
77 reliability: 0,
78 notification_class: instance,
79 priority: [255, 255, 255],
80 ack_required: [false, false, false],
81 recipient_list: Vec::new(),
82 })
83 }
84
85 pub fn set_description(&mut self, desc: impl Into<String>) {
87 self.description = desc.into();
88 }
89
90 pub fn add_destination(&mut self, dest: BACnetDestination) {
92 self.recipient_list.push(dest);
93 }
94}
95
96impl BACnetObject for NotificationClass {
97 fn object_identifier(&self) -> ObjectIdentifier {
98 self.oid
99 }
100
101 fn object_name(&self) -> &str {
102 &self.name
103 }
104
105 fn read_property(
106 &self,
107 property: PropertyIdentifier,
108 array_index: Option<u32>,
109 ) -> Result<PropertyValue, Error> {
110 if let Some(result) = read_common_properties!(self, property, array_index) {
111 return result;
112 }
113 match property {
114 p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
115 ObjectType::NOTIFICATION_CLASS.to_raw(),
116 )),
117 p if p == PropertyIdentifier::EVENT_STATE => {
118 Ok(PropertyValue::Enumerated(0)) }
120 p if p == PropertyIdentifier::NOTIFICATION_CLASS => {
121 Ok(PropertyValue::Unsigned(self.notification_class as u64))
122 }
123 p if p == PropertyIdentifier::PRIORITY => match array_index {
124 Some(0) => Ok(PropertyValue::Unsigned(3)),
125 Some(idx) if (1..=3).contains(&idx) => Ok(PropertyValue::Unsigned(
126 self.priority[(idx - 1) as usize] as u64,
127 )),
128 None => Ok(PropertyValue::List(vec![
129 PropertyValue::Unsigned(self.priority[0] as u64),
130 PropertyValue::Unsigned(self.priority[1] as u64),
131 PropertyValue::Unsigned(self.priority[2] as u64),
132 ])),
133 _ => Err(common::invalid_array_index_error()),
134 },
135 p if p == PropertyIdentifier::ACK_REQUIRED => {
136 let mut byte: u8 = 0;
138 if self.ack_required[0] {
139 byte |= 0x80;
140 } if self.ack_required[1] {
142 byte |= 0x40;
143 } if self.ack_required[2] {
145 byte |= 0x20;
146 } Ok(PropertyValue::BitString {
148 unused_bits: 5,
149 data: vec![byte],
150 })
151 }
152 p if p == PropertyIdentifier::RECIPIENT_LIST => {
153 let mut buf = bytes::BytesMut::new();
158 bacnet_encoding::constructed::encode_destination_list(
159 &mut buf,
160 &self.recipient_list,
161 );
162 Ok(PropertyValue::ApplicationData(buf.to_vec()))
163 }
164 _ => Err(common::unknown_property_error()),
165 }
166 }
167
168 fn write_property(
169 &mut self,
170 property: PropertyIdentifier,
171 array_index: Option<u32>,
172 value: PropertyValue,
173 _priority: Option<u8>,
174 ) -> Result<(), Error> {
175 if property == PropertyIdentifier::NOTIFICATION_CLASS {
176 if let PropertyValue::Unsigned(v) = value {
177 self.notification_class = common::u64_to_u32(v)?;
178 return Ok(());
179 }
180 return Err(common::invalid_data_type_error());
181 }
182 if property == PropertyIdentifier::RECIPIENT_LIST {
183 if array_index.is_some() {
190 return Err(common::property_is_not_an_array_error());
191 }
192 self.recipient_list = match &value {
193 PropertyValue::ApplicationData(bytes) => {
197 match bacnet_encoding::constructed::decode_destination_list(bytes) {
198 Ok(list) => list,
199 Err(_) => return Err(common::invalid_data_type_error()),
200 }
201 }
202 PropertyValue::List(entries) => {
205 let mut new_list = Vec::with_capacity(entries.len());
206 for entry in entries {
207 let PropertyValue::List(fields) = entry else {
208 return Err(common::invalid_data_type_error());
209 };
210 match destination_from_flat_fields(fields) {
211 Some(dest) => new_list.push(dest),
212 None => return Err(common::invalid_data_type_error()),
213 }
214 }
215 new_list
216 }
217 _ => return Err(common::invalid_data_type_error()),
218 };
219 return Ok(());
220 }
221 if let Some(result) =
222 common::write_out_of_service(&mut self.out_of_service, property, &value)
223 {
224 return result;
225 }
226 if let Some(result) = common::write_description(&mut self.description, property, &value) {
227 return result;
228 }
229 Err(common::write_access_denied_error())
230 }
231
232 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
233 static PROPS: &[PropertyIdentifier] = &[
234 PropertyIdentifier::OBJECT_IDENTIFIER,
235 PropertyIdentifier::OBJECT_NAME,
236 PropertyIdentifier::DESCRIPTION,
237 PropertyIdentifier::OBJECT_TYPE,
238 PropertyIdentifier::STATUS_FLAGS,
239 PropertyIdentifier::EVENT_STATE,
240 PropertyIdentifier::OUT_OF_SERVICE,
241 PropertyIdentifier::RELIABILITY,
242 PropertyIdentifier::NOTIFICATION_CLASS,
243 PropertyIdentifier::PRIORITY,
244 PropertyIdentifier::ACK_REQUIRED,
245 PropertyIdentifier::RECIPIENT_LIST,
246 ];
247 Cow::Borrowed(PROPS)
248 }
249}
250
251fn time_to_centiseconds(t: &Time) -> u32 {
253 let h = if t.hour == Time::UNSPECIFIED {
254 0
255 } else {
256 t.hour as u32
257 };
258 let m = if t.minute == Time::UNSPECIFIED {
259 0
260 } else {
261 t.minute as u32
262 };
263 let s = if t.second == Time::UNSPECIFIED {
264 0
265 } else {
266 t.second as u32
267 };
268 let cs = if t.hundredths == Time::UNSPECIFIED {
269 0
270 } else {
271 t.hundredths as u32
272 };
273 h * 360_000 + m * 6_000 + s * 100 + cs
274}
275
276fn time_in_window(current: &Time, from: &Time, to: &Time) -> bool {
285 if from.hour == Time::UNSPECIFIED || to.hour == Time::UNSPECIFIED {
286 return true;
287 }
288 let cur = time_to_centiseconds(current);
289 let from_cs = time_to_centiseconds(from);
290 let to_cs = time_to_centiseconds(to);
291 if to_cs < from_cs {
292 cur >= from_cs || cur <= to_cs
294 } else {
295 cur >= from_cs && cur <= to_cs
296 }
297}
298
299pub fn local_day_and_time(utc_secs: u64, utc_offset_minutes: i32) -> (u8, Time) {
309 let local_secs = utc_secs.saturating_add_signed(-i64::from(utc_offset_minutes) * 60);
312 let dow = ((local_secs / 86400 + 3) % 7) as u8;
313 let today_bit = 1u8 << dow;
314 let day_secs = (local_secs % 86400) as u32;
315 let current_time = Time {
316 hour: (day_secs / 3600) as u8,
317 minute: ((day_secs % 3600) / 60) as u8,
318 second: (day_secs % 60) as u8,
319 hundredths: 0,
320 };
321 (today_bit, current_time)
322}
323
324fn find_notification_class(
331 db: &ObjectDatabase,
332 notification_class: u32,
333) -> Option<&dyn BACnetObject> {
334 if let Ok(nc_oid) = ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, notification_class) {
336 if let Some(obj) = db.get(&nc_oid) {
337 if matches!(
338 obj.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None),
339 Ok(PropertyValue::Unsigned(n)) if n as u32 == notification_class
340 ) {
341 return Some(obj);
342 }
343 }
344 }
345
346 db.find_by_type(ObjectType::NOTIFICATION_CLASS)
348 .iter()
349 .find_map(|oid| {
350 let obj = db.get(oid)?;
351 match obj.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None) {
352 Ok(PropertyValue::Unsigned(n)) if n as u32 == notification_class => Some(obj),
353 _ => None,
354 }
355 })
356}
357
358pub fn resolve_transition_priority_ack(
378 db: &ObjectDatabase,
379 notification_class: u32,
380 transition: EventTransition,
381) -> (u8, bool) {
382 let Some(nc) = find_notification_class(db, notification_class) else {
383 return (255, false);
384 };
385 let idx = transition.index();
386
387 let priority = nc
392 .read_property(PropertyIdentifier::PRIORITY, Some(idx as u32 + 1))
393 .ok()
394 .and_then(|v| match v {
395 PropertyValue::Unsigned(n) => Some(n as u8),
396 _ => None,
397 })
398 .unwrap_or(255);
399
400 let ack_required = nc
403 .read_property(PropertyIdentifier::ACK_REQUIRED, None)
404 .ok()
405 .and_then(|v| match v {
406 PropertyValue::BitString { data, .. } => data.first().copied(),
407 _ => None,
408 })
409 .map(|byte| byte & (0x80 >> idx) != 0)
410 .unwrap_or(false);
411
412 (priority, ack_required)
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
422pub enum RecipientLookupOutcome {
423 NotificationClassMissing,
425 RecipientListUnavailable,
427 RecipientListInvalid,
429 NoConfiguredDestinations,
431 NoMatchingDestinations,
433 Matched(Vec<(BACnetRecipient, u32, bool)>),
435}
436
437pub fn lookup_notification_recipients(
449 db: &ObjectDatabase,
450 notification_class: u32,
451 transition: EventTransition,
452 today_bit: u8,
453 current_time: &Time,
454) -> RecipientLookupOutcome {
455 let Some(nc) = find_notification_class(db, notification_class) else {
456 return RecipientLookupOutcome::NotificationClassMissing;
457 };
458 let Ok(recipient_list_value) = nc.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
459 else {
460 return RecipientLookupOutcome::RecipientListUnavailable;
461 };
462 let Ok(destinations) = decode_destination_list_pv(&recipient_list_value) else {
463 return RecipientLookupOutcome::RecipientListInvalid;
464 };
465 if destinations.is_empty() {
466 return RecipientLookupOutcome::NoConfiguredDestinations;
467 }
468
469 let recipients = filter_destinations(destinations, transition, today_bit, current_time);
470 if recipients.is_empty() {
471 RecipientLookupOutcome::NoMatchingDestinations
472 } else {
473 RecipientLookupOutcome::Matched(recipients)
474 }
475}
476
477pub fn get_notification_recipients(
483 db: &ObjectDatabase,
484 notification_class: u32,
485 transition: EventTransition,
486 today_bit: u8,
487 current_time: &Time,
488) -> Vec<(BACnetRecipient, u32, bool)> {
489 match lookup_notification_recipients(
490 db,
491 notification_class,
492 transition,
493 today_bit,
494 current_time,
495 ) {
496 RecipientLookupOutcome::Matched(recipients) => recipients,
497 RecipientLookupOutcome::NotificationClassMissing
498 | RecipientLookupOutcome::RecipientListUnavailable
499 | RecipientLookupOutcome::RecipientListInvalid
500 | RecipientLookupOutcome::NoConfiguredDestinations
501 | RecipientLookupOutcome::NoMatchingDestinations => Vec::new(),
502 }
503}
504
505pub fn get_notification_recipients_strict(
513 db: &ObjectDatabase,
514 notification_class: u32,
515 transition: EventTransition,
516 today_bit: u8,
517 current_time: &Time,
518) -> Option<Vec<(BACnetRecipient, u32, bool)>> {
519 match lookup_notification_recipients(
520 db,
521 notification_class,
522 transition,
523 today_bit,
524 current_time,
525 ) {
526 RecipientLookupOutcome::RecipientListInvalid => None,
527 RecipientLookupOutcome::Matched(recipients) => Some(recipients),
528 RecipientLookupOutcome::NotificationClassMissing
529 | RecipientLookupOutcome::RecipientListUnavailable
530 | RecipientLookupOutcome::NoConfiguredDestinations
531 | RecipientLookupOutcome::NoMatchingDestinations => Some(Vec::new()),
532 }
533}
534
535fn destination_from_flat_fields(fields: &[PropertyValue]) -> Option<BACnetDestination> {
544 if fields.len() < 7 {
545 return None;
546 }
547 let valid_days = match &fields[0] {
549 PropertyValue::BitString { data, .. } if !data.is_empty() => {
550 bacnet_types::bitstring::unpack_octet(data, 7)
551 }
552 _ => return None,
553 };
554 let from_time = match fields[1] {
556 PropertyValue::Time(t) => t,
557 _ => return None,
558 };
559 let to_time = match fields[2] {
561 PropertyValue::Time(t) => t,
562 _ => return None,
563 };
564 let recipient = match &fields[3] {
567 PropertyValue::ObjectIdentifier(oid) => BACnetRecipient::Device(*oid),
568 PropertyValue::List(items) if items.len() == 2 => {
569 let network_number = match &items[0] {
570 PropertyValue::Unsigned(v) => *v as u16,
571 _ => return None,
572 };
573 let mac_address = match &items[1] {
574 PropertyValue::OctetString(mac) => MacAddr::from_slice(mac),
575 _ => return None,
576 };
577 BACnetRecipient::Address(BACnetAddress {
578 network_number,
579 mac_address,
580 })
581 }
582 _ => return None,
583 };
584 let process_identifier = match fields[4] {
586 PropertyValue::Unsigned(v) => u32::try_from(v).ok()?,
587 _ => return None,
588 };
589 let issue_confirmed_notifications = match fields[5] {
591 PropertyValue::Boolean(b) => b,
592 _ => return None,
593 };
594 let transitions = match &fields[6] {
596 PropertyValue::BitString { data, .. } if !data.is_empty() => {
597 bacnet_types::bitstring::unpack_octet(data, 3)
598 }
599 _ => return None,
600 };
601 Some(BACnetDestination {
602 valid_days,
603 from_time,
604 to_time,
605 recipient,
606 process_identifier,
607 issue_confirmed_notifications,
608 transitions,
609 })
610}
611
612pub(super) fn decode_destination_list_pv(
621 value: &PropertyValue,
622) -> Result<Vec<BACnetDestination>, Error> {
623 match value {
624 PropertyValue::ApplicationData(bytes) => {
625 bacnet_encoding::constructed::decode_destination_list(bytes)
626 }
627 PropertyValue::List(entries) => entries
628 .iter()
629 .map(|entry| match entry {
630 PropertyValue::List(fields) => destination_from_flat_fields(fields)
631 .ok_or_else(|| Error::decoding(0, "Recipient_List: malformed legacy entry")),
632 _ => Err(Error::decoding(
633 0,
634 "Recipient_List: entry is not a field list",
635 )),
636 })
637 .collect(),
638 _ => Err(Error::decoding(
639 0,
640 "Recipient_List: expected framed application data or a legacy list",
641 )),
642 }
643}
644
645fn filter_destinations(
649 destinations: Vec<BACnetDestination>,
650 transition: EventTransition,
651 today_bit: u8,
652 current_time: &Time,
653) -> Vec<(BACnetRecipient, u32, bool)> {
654 let transition_mask = transition.bit_mask();
655 destinations
656 .into_iter()
657 .filter(|dest| dest.valid_days & today_bit != 0)
658 .filter(|dest| time_in_window(current_time, &dest.from_time, &dest.to_time))
659 .filter(|dest| dest.transitions & transition_mask != 0)
660 .map(|dest| {
661 (
662 dest.recipient,
663 dest.process_identifier,
664 dest.issue_confirmed_notifications,
665 )
666 })
667 .collect()
668}
669
670pub fn filter_recipient_list(
679 recipient_list_value: &PropertyValue,
680 transition: EventTransition,
681 today_bit: u8,
682 current_time: &Time,
683) -> Vec<(BACnetRecipient, u32, bool)> {
684 let Ok(destinations) = decode_destination_list_pv(recipient_list_value) else {
685 return Vec::new();
686 };
687 filter_destinations(destinations, transition, today_bit, current_time)
688}
689
690#[cfg(test)]
691mod tests;