use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags, Time};
use bacnet_types::MacAddr;
use std::borrow::Cow;
use crate::common::{self, read_common_properties};
use crate::database::ObjectDatabase;
use crate::event::EventTransition;
use crate::traits::BACnetObject;
mod enrollment_summary;
#[doc(hidden)]
pub use enrollment_summary::{
resolve_enrollment_summary_class_internal, EnrollmentSummaryClassProjection,
EnrollmentSummaryClassProjectionError,
};
pub struct NotificationClass {
oid: ObjectIdentifier,
name: String,
description: String,
status_flags: StatusFlags,
out_of_service: bool,
reliability: u32,
pub notification_class: u32,
pub priority: [u8; 3],
pub ack_required: [bool; 3],
pub recipient_list: Vec<BACnetDestination>,
}
impl NotificationClass {
pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
status_flags: StatusFlags::empty(),
out_of_service: false,
reliability: 0,
notification_class: instance,
priority: [255, 255, 255],
ack_required: [false, false, false],
recipient_list: Vec::new(),
})
}
pub fn set_description(&mut self, desc: impl Into<String>) {
self.description = desc.into();
}
pub fn add_destination(&mut self, dest: BACnetDestination) {
self.recipient_list.push(dest);
}
}
impl BACnetObject for NotificationClass {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
&self.name
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if let Some(result) = read_common_properties!(self, property, array_index) {
return result;
}
match property {
p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
ObjectType::NOTIFICATION_CLASS.to_raw(),
)),
p if p == PropertyIdentifier::EVENT_STATE => {
Ok(PropertyValue::Enumerated(0)) }
p if p == PropertyIdentifier::NOTIFICATION_CLASS => {
Ok(PropertyValue::Unsigned(self.notification_class as u64))
}
p if p == PropertyIdentifier::PRIORITY => match array_index {
Some(0) => Ok(PropertyValue::Unsigned(3)),
Some(idx) if (1..=3).contains(&idx) => Ok(PropertyValue::Unsigned(
self.priority[(idx - 1) as usize] as u64,
)),
None => Ok(PropertyValue::List(vec![
PropertyValue::Unsigned(self.priority[0] as u64),
PropertyValue::Unsigned(self.priority[1] as u64),
PropertyValue::Unsigned(self.priority[2] as u64),
])),
_ => Err(common::invalid_array_index_error()),
},
p if p == PropertyIdentifier::ACK_REQUIRED => {
let mut byte: u8 = 0;
if self.ack_required[0] {
byte |= 0x80;
} if self.ack_required[1] {
byte |= 0x40;
} if self.ack_required[2] {
byte |= 0x20;
} Ok(PropertyValue::BitString {
unused_bits: 5,
data: vec![byte],
})
}
p if p == PropertyIdentifier::RECIPIENT_LIST => {
let mut buf = bytes::BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(
&mut buf,
&self.recipient_list,
);
Ok(PropertyValue::ApplicationData(buf.to_vec()))
}
_ => Err(common::unknown_property_error()),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
if property == PropertyIdentifier::NOTIFICATION_CLASS {
if let PropertyValue::Unsigned(v) = value {
self.notification_class = common::u64_to_u32(v)?;
return Ok(());
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::RECIPIENT_LIST {
if array_index.is_some() {
return Err(common::property_is_not_an_array_error());
}
self.recipient_list = match &value {
PropertyValue::ApplicationData(bytes) => {
match bacnet_encoding::constructed::decode_destination_list(bytes) {
Ok(list) => list,
Err(_) => return Err(common::invalid_data_type_error()),
}
}
PropertyValue::List(entries) => {
let mut new_list = Vec::with_capacity(entries.len());
for entry in entries {
let PropertyValue::List(fields) = entry else {
return Err(common::invalid_data_type_error());
};
match destination_from_flat_fields(fields) {
Some(dest) => new_list.push(dest),
None => return Err(common::invalid_data_type_error()),
}
}
new_list
}
_ => return Err(common::invalid_data_type_error()),
};
return Ok(());
}
if let Some(result) =
common::write_out_of_service(&mut self.out_of_service, property, &value)
{
return result;
}
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
Err(common::write_access_denied_error())
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
static PROPS: &[PropertyIdentifier] = &[
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::OBJECT_TYPE,
PropertyIdentifier::STATUS_FLAGS,
PropertyIdentifier::EVENT_STATE,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::RELIABILITY,
PropertyIdentifier::NOTIFICATION_CLASS,
PropertyIdentifier::PRIORITY,
PropertyIdentifier::ACK_REQUIRED,
PropertyIdentifier::RECIPIENT_LIST,
];
Cow::Borrowed(PROPS)
}
}
fn time_to_centiseconds(t: &Time) -> u32 {
let h = if t.hour == Time::UNSPECIFIED {
0
} else {
t.hour as u32
};
let m = if t.minute == Time::UNSPECIFIED {
0
} else {
t.minute as u32
};
let s = if t.second == Time::UNSPECIFIED {
0
} else {
t.second as u32
};
let cs = if t.hundredths == Time::UNSPECIFIED {
0
} else {
t.hundredths as u32
};
h * 360_000 + m * 6_000 + s * 100 + cs
}
fn time_in_window(current: &Time, from: &Time, to: &Time) -> bool {
if from.hour == Time::UNSPECIFIED || to.hour == Time::UNSPECIFIED {
return true;
}
let cur = time_to_centiseconds(current);
let from_cs = time_to_centiseconds(from);
let to_cs = time_to_centiseconds(to);
if to_cs < from_cs {
cur >= from_cs || cur <= to_cs
} else {
cur >= from_cs && cur <= to_cs
}
}
pub fn local_day_and_time(utc_secs: u64, utc_offset_minutes: i32) -> (u8, Time) {
let local_secs = utc_secs.saturating_add_signed(-i64::from(utc_offset_minutes) * 60);
let dow = ((local_secs / 86400 + 3) % 7) as u8;
let today_bit = 1u8 << dow;
let day_secs = (local_secs % 86400) as u32;
let current_time = Time {
hour: (day_secs / 3600) as u8,
minute: ((day_secs % 3600) / 60) as u8,
second: (day_secs % 60) as u8,
hundredths: 0,
};
(today_bit, current_time)
}
fn find_notification_class(
db: &ObjectDatabase,
notification_class: u32,
) -> Option<&dyn BACnetObject> {
if let Ok(nc_oid) = ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, notification_class) {
if let Some(obj) = db.get(&nc_oid) {
if matches!(
obj.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None),
Ok(PropertyValue::Unsigned(n)) if n as u32 == notification_class
) {
return Some(obj);
}
}
}
db.find_by_type(ObjectType::NOTIFICATION_CLASS)
.iter()
.find_map(|oid| {
let obj = db.get(oid)?;
match obj.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None) {
Ok(PropertyValue::Unsigned(n)) if n as u32 == notification_class => Some(obj),
_ => None,
}
})
}
pub fn resolve_transition_priority_ack(
db: &ObjectDatabase,
notification_class: u32,
transition: EventTransition,
) -> (u8, bool) {
let Some(nc) = find_notification_class(db, notification_class) else {
return (255, false);
};
let idx = transition.index();
let priority = nc
.read_property(PropertyIdentifier::PRIORITY, Some(idx as u32 + 1))
.ok()
.and_then(|v| match v {
PropertyValue::Unsigned(n) => Some(n as u8),
_ => None,
})
.unwrap_or(255);
let ack_required = nc
.read_property(PropertyIdentifier::ACK_REQUIRED, None)
.ok()
.and_then(|v| match v {
PropertyValue::BitString { data, .. } => data.first().copied(),
_ => None,
})
.map(|byte| byte & (0x80 >> idx) != 0)
.unwrap_or(false);
(priority, ack_required)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecipientLookupOutcome {
NotificationClassMissing,
RecipientListUnavailable,
RecipientListInvalid,
NoConfiguredDestinations,
NoMatchingDestinations,
Matched(Vec<(BACnetRecipient, u32, bool)>),
}
pub fn lookup_notification_recipients(
db: &ObjectDatabase,
notification_class: u32,
transition: EventTransition,
today_bit: u8,
current_time: &Time,
) -> RecipientLookupOutcome {
let Some(nc) = find_notification_class(db, notification_class) else {
return RecipientLookupOutcome::NotificationClassMissing;
};
let Ok(recipient_list_value) = nc.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
else {
return RecipientLookupOutcome::RecipientListUnavailable;
};
let Ok(destinations) = decode_destination_list_pv(&recipient_list_value) else {
return RecipientLookupOutcome::RecipientListInvalid;
};
if destinations.is_empty() {
return RecipientLookupOutcome::NoConfiguredDestinations;
}
let recipients = filter_destinations(destinations, transition, today_bit, current_time);
if recipients.is_empty() {
RecipientLookupOutcome::NoMatchingDestinations
} else {
RecipientLookupOutcome::Matched(recipients)
}
}
pub fn get_notification_recipients(
db: &ObjectDatabase,
notification_class: u32,
transition: EventTransition,
today_bit: u8,
current_time: &Time,
) -> Vec<(BACnetRecipient, u32, bool)> {
match lookup_notification_recipients(
db,
notification_class,
transition,
today_bit,
current_time,
) {
RecipientLookupOutcome::Matched(recipients) => recipients,
RecipientLookupOutcome::NotificationClassMissing
| RecipientLookupOutcome::RecipientListUnavailable
| RecipientLookupOutcome::RecipientListInvalid
| RecipientLookupOutcome::NoConfiguredDestinations
| RecipientLookupOutcome::NoMatchingDestinations => Vec::new(),
}
}
pub fn get_notification_recipients_strict(
db: &ObjectDatabase,
notification_class: u32,
transition: EventTransition,
today_bit: u8,
current_time: &Time,
) -> Option<Vec<(BACnetRecipient, u32, bool)>> {
match lookup_notification_recipients(
db,
notification_class,
transition,
today_bit,
current_time,
) {
RecipientLookupOutcome::RecipientListInvalid => None,
RecipientLookupOutcome::Matched(recipients) => Some(recipients),
RecipientLookupOutcome::NotificationClassMissing
| RecipientLookupOutcome::RecipientListUnavailable
| RecipientLookupOutcome::NoConfiguredDestinations
| RecipientLookupOutcome::NoMatchingDestinations => Some(Vec::new()),
}
}
fn destination_from_flat_fields(fields: &[PropertyValue]) -> Option<BACnetDestination> {
if fields.len() < 7 {
return None;
}
let valid_days = match &fields[0] {
PropertyValue::BitString { data, .. } if !data.is_empty() => {
bacnet_types::bitstring::unpack_octet(data, 7)
}
_ => return None,
};
let from_time = match fields[1] {
PropertyValue::Time(t) => t,
_ => return None,
};
let to_time = match fields[2] {
PropertyValue::Time(t) => t,
_ => return None,
};
let recipient = match &fields[3] {
PropertyValue::ObjectIdentifier(oid) => BACnetRecipient::Device(*oid),
PropertyValue::List(items) if items.len() == 2 => {
let network_number = match &items[0] {
PropertyValue::Unsigned(v) => *v as u16,
_ => return None,
};
let mac_address = match &items[1] {
PropertyValue::OctetString(mac) => MacAddr::from_slice(mac),
_ => return None,
};
BACnetRecipient::Address(BACnetAddress {
network_number,
mac_address,
})
}
_ => return None,
};
let process_identifier = match fields[4] {
PropertyValue::Unsigned(v) => u32::try_from(v).ok()?,
_ => return None,
};
let issue_confirmed_notifications = match fields[5] {
PropertyValue::Boolean(b) => b,
_ => return None,
};
let transitions = match &fields[6] {
PropertyValue::BitString { data, .. } if !data.is_empty() => {
bacnet_types::bitstring::unpack_octet(data, 3)
}
_ => return None,
};
Some(BACnetDestination {
valid_days,
from_time,
to_time,
recipient,
process_identifier,
issue_confirmed_notifications,
transitions,
})
}
pub(super) fn decode_destination_list_pv(
value: &PropertyValue,
) -> Result<Vec<BACnetDestination>, Error> {
match value {
PropertyValue::ApplicationData(bytes) => {
bacnet_encoding::constructed::decode_destination_list(bytes)
}
PropertyValue::List(entries) => entries
.iter()
.map(|entry| match entry {
PropertyValue::List(fields) => destination_from_flat_fields(fields)
.ok_or_else(|| Error::decoding(0, "Recipient_List: malformed legacy entry")),
_ => Err(Error::decoding(
0,
"Recipient_List: entry is not a field list",
)),
})
.collect(),
_ => Err(Error::decoding(
0,
"Recipient_List: expected framed application data or a legacy list",
)),
}
}
fn filter_destinations(
destinations: Vec<BACnetDestination>,
transition: EventTransition,
today_bit: u8,
current_time: &Time,
) -> Vec<(BACnetRecipient, u32, bool)> {
let transition_mask = transition.bit_mask();
destinations
.into_iter()
.filter(|dest| dest.valid_days & today_bit != 0)
.filter(|dest| time_in_window(current_time, &dest.from_time, &dest.to_time))
.filter(|dest| dest.transitions & transition_mask != 0)
.map(|dest| {
(
dest.recipient,
dest.process_identifier,
dest.issue_confirmed_notifications,
)
})
.collect()
}
pub fn filter_recipient_list(
recipient_list_value: &PropertyValue,
transition: EventTransition,
today_bit: u8,
current_time: &Time,
) -> Vec<(BACnetRecipient, u32, bool)> {
let Ok(destinations) = decode_destination_list_pv(recipient_list_value) else {
return Vec::new();
};
filter_destinations(destinations, transition, today_bit, current_time)
}
#[cfg(test)]
mod tests;