use super::super::*;
use super::make_time;
use bacnet_types::constructed::{BACnetDestination, BACnetRecipient};
use bacnet_types::primitives::Time;
fn make_dest(
device_instance: u32,
valid_days: u8,
from: Time,
to: Time,
confirmed: bool,
transitions: u8,
) -> BACnetDestination {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, device_instance).unwrap();
BACnetDestination {
valid_days,
from_time: from,
to_time: to,
recipient: BACnetRecipient::Device(dev_oid),
process_identifier: device_instance,
issue_confirmed_notifications: confirmed,
transitions,
}
}
#[test]
fn get_recipients_filters_by_transition() {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.add_destination(make_dest(
10,
0b0111_1111,
make_time(0, 0),
make_time(23, 59),
false,
0b0000_0001,
));
nc.add_destination(make_dest(
20,
0b0111_1111,
make_time(0, 0),
make_time(23, 59),
true,
0b0000_0100,
));
nc.add_destination(make_dest(
30,
0b0111_1111,
make_time(0, 0),
make_time(23, 59),
false,
0b0000_0111,
));
db.add(Box::new(nc)).unwrap();
let now = make_time(12, 0);
let monday_bit = 0x01;
let r = get_notification_recipients(&db, 1, EventTransition::ToOffnormal, monday_bit, &now);
assert_eq!(r.len(), 2);
assert_eq!(r[0].1, 10); assert_eq!(r[1].1, 30);
let r = get_notification_recipients(&db, 1, EventTransition::ToNormal, monday_bit, &now);
assert_eq!(r.len(), 2);
assert_eq!(r[0].1, 20);
assert!(r[0].2); assert_eq!(r[1].1, 30);
let r = get_notification_recipients(&db, 1, EventTransition::ToFault, monday_bit, &now);
assert_eq!(r.len(), 1);
assert_eq!(r[0].1, 30);
}
#[test]
fn get_recipients_filters_by_day() {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(2, "NC-2").unwrap();
nc.add_destination(make_dest(
10,
0b0001_1111,
make_time(0, 0),
make_time(23, 59),
false,
0b0000_0111,
));
db.add(Box::new(nc)).unwrap();
let now = make_time(12, 0);
let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x01, &now);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x10, &now);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x40, &now);
assert!(r.is_empty());
let r = get_notification_recipients(&db, 2, EventTransition::ToOffnormal, 0x20, &now);
assert!(r.is_empty());
}
#[test]
fn get_recipients_filters_by_time_window() {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(3, "NC-3").unwrap();
nc.add_destination(make_dest(
10,
0b0111_1111,
make_time(8, 0),
make_time(17, 0),
false,
0b0000_0111,
));
db.add(Box::new(nc)).unwrap();
let monday_bit = 0x01;
let r = get_notification_recipients(
&db,
3,
EventTransition::ToOffnormal,
monday_bit,
&make_time(12, 0),
);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(
&db,
3,
EventTransition::ToOffnormal,
monday_bit,
&make_time(7, 0),
);
assert!(r.is_empty());
let r = get_notification_recipients(
&db,
3,
EventTransition::ToOffnormal,
monday_bit,
&make_time(18, 0),
);
assert!(r.is_empty());
}
#[test]
fn get_recipients_time_window_boundary_inclusive() {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(4, "NC-4").unwrap();
nc.add_destination(make_dest(
10,
0b0111_1111,
make_time(8, 0),
make_time(17, 0),
false,
0b0000_0111,
));
db.add(Box::new(nc)).unwrap();
let monday_bit = 0x01;
let r = get_notification_recipients(
&db,
4,
EventTransition::ToOffnormal,
monday_bit,
&make_time(8, 0),
);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(
&db,
4,
EventTransition::ToOffnormal,
monday_bit,
&make_time(17, 0),
);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(
&db,
4,
EventTransition::ToOffnormal,
monday_bit,
&make_time(7, 59),
);
assert!(r.is_empty());
}
#[test]
fn get_recipients_overnight_window_crosses_midnight() {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(5, "NC-5").unwrap();
nc.add_destination(make_dest(
10,
0b0111_1111,
make_time(22, 0),
make_time(2, 0),
false,
0b0000_0111,
));
db.add(Box::new(nc)).unwrap();
let monday_bit = 0x01;
let r = get_notification_recipients(
&db,
5,
EventTransition::ToOffnormal,
monday_bit,
&make_time(23, 0),
);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(
&db,
5,
EventTransition::ToOffnormal,
monday_bit,
&make_time(1, 0),
);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(
&db,
5,
EventTransition::ToOffnormal,
monday_bit,
&make_time(12, 0),
);
assert!(r.is_empty());
let r = get_notification_recipients(
&db,
5,
EventTransition::ToOffnormal,
monday_bit,
&make_time(3, 0),
);
assert!(r.is_empty());
}
#[test]
fn get_recipients_day_filter_sunday_and_weekend_bits() {
let mut db = ObjectDatabase::new();
let mut nc = NotificationClass::new(6, "NC-6").unwrap();
nc.add_destination(make_dest(
10,
0b0100_0000,
make_time(0, 0),
make_time(23, 59),
false,
0b0000_0111,
));
db.add(Box::new(nc)).unwrap();
let now = make_time(12, 0);
let r = get_notification_recipients(&db, 6, EventTransition::ToOffnormal, 0x40, &now);
assert_eq!(r.len(), 1);
let r = get_notification_recipients(&db, 6, EventTransition::ToOffnormal, 0x01, &now);
assert!(r.is_empty());
let r = get_notification_recipients(&db, 6, EventTransition::ToOffnormal, 0x20, &now);
assert!(r.is_empty());
}
#[test]
fn local_day_and_time_day_bits_match_bacnet_days_of_week() {
assert_eq!(local_day_and_time(0, 0).0, 0x08, "epoch Thursday = bit 3");
assert_eq!(local_day_and_time(3 * 86400, 0).0, 0x40, "Sunday = bit 6");
assert_eq!(local_day_and_time(4 * 86400, 0).0, 0x01, "Monday = bit 0");
}
#[test]
fn local_day_and_time_offset_shifts_day_and_time() {
let (bit, t) = local_day_and_time(23 * 3600, -60);
assert_eq!(bit, 0x10, "UTC 23:00 Thu +60min rolls to Friday bit 4");
assert_eq!((t.hour, t.minute, t.second), (0, 0, 0));
let (bit, t) = local_day_and_time(86400 + 30 * 60, 60);
assert_eq!(
bit, 0x08,
"UTC 00:30 next day -60min rolls back to Thursday"
);
assert_eq!((t.hour, t.minute), (23, 30));
}
#[test]
fn local_day_and_time_time_of_day_preserved_under_zero_offset() {
let (_, t) = local_day_and_time(12 * 3600 + 34 * 60 + 56, 0);
assert_eq!((t.hour, t.minute, t.second), (12, 34, 56));
}
#[test]
fn get_recipients_returns_empty_for_missing_class() {
let db = ObjectDatabase::new();
let r = get_notification_recipients(
&db,
99,
EventTransition::ToOffnormal,
0x01,
&make_time(12, 0),
);
assert!(r.is_empty());
}
#[test]
fn get_recipients_returns_empty_for_empty_list() {
let mut db = ObjectDatabase::new();
let nc = NotificationClass::new(1, "NC-1").unwrap();
db.add(Box::new(nc)).unwrap();
let r = get_notification_recipients(
&db,
1,
EventTransition::ToOffnormal,
0x01,
&make_time(12, 0),
);
assert!(r.is_empty());
}
#[test]
fn event_state_change_transition_mapping() {
use crate::event::EventStateChange;
use bacnet_types::enums::EventState;
let to_normal = EventStateChange {
from: EventState::HIGH_LIMIT,
to: EventState::NORMAL,
};
assert_eq!(to_normal.transition(), EventTransition::ToNormal);
let to_fault = EventStateChange {
from: EventState::NORMAL,
to: EventState::FAULT,
};
assert_eq!(to_fault.transition(), EventTransition::ToFault);
let to_high = EventStateChange {
from: EventState::NORMAL,
to: EventState::HIGH_LIMIT,
};
assert_eq!(to_high.transition(), EventTransition::ToOffnormal);
let to_low = EventStateChange {
from: EventState::NORMAL,
to: EventState::LOW_LIMIT,
};
assert_eq!(to_low.transition(), EventTransition::ToOffnormal);
}