use bacnet_types::bitstring::{DaysOfWeek, EventTransitionBits};
use bacnet_types::constructed::{BACnetDestination, BACnetRecipient};
use bacnet_types::enums::{EventState, ObjectType, PropertyIdentifier, Reliability};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags, Time};
use std::borrow::Cow;
use crate::common::{self, read_common_properties};
use crate::database::ObjectDatabase;
use crate::durable::DurableWrites;
use crate::event::EventTransition;
use crate::traits::BACnetObject;
mod enrollment_summary;
mod metadata;
mod persistence;
pub(crate) mod recipient_list;
mod saving;
#[doc(hidden)]
pub use enrollment_summary::{
resolve_enrollment_summary_class_internal, EnrollmentSummaryClassProjection,
EnrollmentSummaryClassProjectionError,
};
pub use persistence::{
FileNotificationClassPersistence, NotificationClassPersistence, NotificationClassSnapshot,
};
pub use recipient_list::MAX_RECIPIENT_LIST_DESTINATIONS;
pub struct NotificationClass {
oid: ObjectIdentifier,
name: String,
description: String,
status_flags: StatusFlags,
reliability: Reliability,
pub notification_class: u32,
pub priority: [u8; 3],
pub ack_required: EventTransitionBits,
recipient_list: Vec<BACnetDestination>,
storage: Option<saving::Storage>,
list_writes: u64,
recipient_list_written: bool,
}
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(),
reliability: Reliability::NO_FAULT_DETECTED,
notification_class: instance,
priority: [255, 255, 255],
ack_required: EventTransitionBits::empty(),
recipient_list: Vec::new(),
storage: None,
list_writes: 0,
recipient_list_written: false,
})
}
pub fn set_description(&mut self, desc: impl Into<String>) {
self.description = desc.into();
}
pub fn add_destination(&mut self, dest: BACnetDestination) -> Result<(), Error> {
recipient_list::check_added(&dest)?;
if self.recipient_list_saved() {
tracing::debug!(
class = %self.oid,
"Saved Recipient_List kept over a configured destination"
);
return Ok(());
}
if self.recipient_list.len() >= MAX_RECIPIENT_LIST_DESTINATIONS {
return Err(recipient_list::no_space_error());
}
self.recipient_list.push(dest);
Ok(())
}
pub fn recipient_list(&self) -> &[BACnetDestination] {
&self.recipient_list
}
}
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, no_out_of_service)
{
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(EventState::NORMAL.to_raw()))
}
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 => Ok(PropertyValue::BitString {
unused_bits: 5,
data: vec![self.ack_required.to_bacnet()],
}),
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());
}
return self.write_recipient_list(value);
}
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
Err(crate::common::unhandled_write_error(
self.property_metadata().as_ref(),
property,
array_index,
))
}
fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
metadata::for_object(self)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
}
fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
self.expire_staged_write(now);
false
}
fn durable_writes_internal(&mut self) -> Option<&mut dyn DurableWrites> {
Some(self)
}
}
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) -> (DaysOfWeek, Time) {
let local_secs = utc_secs.saturating_add_signed(-i64::from(utc_offset_minutes) * 60);
let dow = (local_secs / 86400 + 3) % 7;
let today = DaysOfWeek::from_bits_truncate(1 << 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, 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 = match nc.read_property(PropertyIdentifier::ACK_REQUIRED, None) {
Ok(PropertyValue::BitString { data, .. }) => {
EventTransitionBits::from_bacnet(&data).intersects(transition.bit_mask())
}
_ => false,
};
(priority, ack_required)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecipientLookupOutcome {
NotificationClassMissing,
RecipientListUnavailable,
RecipientListInvalid,
RecipientListTooLong,
NoConfiguredDestinations,
NoMatchingDestinations,
Matched(Vec<(BACnetRecipient, u32, bool)>),
}
pub fn lookup_notification_recipients(
db: &ObjectDatabase,
notification_class: u32,
transition: EventTransition,
today: DaysOfWeek,
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 destinations = match routed_destinations(&recipient_list_value) {
Ok(destinations) => destinations,
Err(outcome) => return outcome,
};
if destinations.is_empty() {
return RecipientLookupOutcome::NoConfiguredDestinations;
}
let recipients = filter_destinations(destinations, transition, today, 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: DaysOfWeek,
current_time: &Time,
) -> Vec<(BACnetRecipient, u32, bool)> {
match lookup_notification_recipients(db, notification_class, transition, today, current_time) {
RecipientLookupOutcome::Matched(recipients) => recipients,
RecipientLookupOutcome::NotificationClassMissing
| RecipientLookupOutcome::RecipientListUnavailable
| RecipientLookupOutcome::RecipientListInvalid
| RecipientLookupOutcome::RecipientListTooLong
| RecipientLookupOutcome::NoConfiguredDestinations
| RecipientLookupOutcome::NoMatchingDestinations => Vec::new(),
}
}
pub fn get_notification_recipients_strict(
db: &ObjectDatabase,
notification_class: u32,
transition: EventTransition,
today: DaysOfWeek,
current_time: &Time,
) -> Option<Vec<(BACnetRecipient, u32, bool)>> {
match lookup_notification_recipients(db, notification_class, transition, today, current_time) {
RecipientLookupOutcome::RecipientListInvalid
| RecipientLookupOutcome::RecipientListTooLong => None,
RecipientLookupOutcome::Matched(recipients) => Some(recipients),
RecipientLookupOutcome::NotificationClassMissing
| RecipientLookupOutcome::RecipientListUnavailable
| RecipientLookupOutcome::NoConfiguredDestinations
| RecipientLookupOutcome::NoMatchingDestinations => Some(Vec::new()),
}
}
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)
}
_ => Err(Error::decoding(
0,
"Recipient_List: expected framed application data",
)),
}
}
fn routed_destinations(
value: &PropertyValue,
) -> Result<Vec<BACnetDestination>, RecipientLookupOutcome> {
let PropertyValue::ApplicationData(bytes) = value else {
return Err(RecipientLookupOutcome::RecipientListInvalid);
};
recipient_list::decode_capped(bytes).map_err(|error| match error {
recipient_list::CappedListError::Malformed => RecipientLookupOutcome::RecipientListInvalid,
recipient_list::CappedListError::PastTheCap(_) => {
RecipientLookupOutcome::RecipientListTooLong
}
})
}
fn filter_destinations(
destinations: Vec<BACnetDestination>,
transition: EventTransition,
today: DaysOfWeek,
current_time: &Time,
) -> Vec<(BACnetRecipient, u32, bool)> {
let transition_mask = transition.bit_mask();
destinations
.into_iter()
.filter(|dest| dest.valid_days.intersects(today))
.filter(|dest| time_in_window(current_time, &dest.from_time, &dest.to_time))
.filter(|dest| dest.transitions.intersects(transition_mask))
.map(|dest| {
(
dest.recipient,
dest.process_identifier,
dest.issue_confirmed_notifications,
)
})
.collect()
}
pub fn filter_recipient_list(
recipient_list_value: &PropertyValue,
transition: EventTransition,
today: DaysOfWeek,
current_time: &Time,
) -> Vec<(BACnetRecipient, u32, bool)> {
let Ok(destinations) = routed_destinations(recipient_list_value) else {
return Vec::new();
};
filter_destinations(destinations, transition, today, current_time)
}
#[cfg(test)]
mod tests;