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;