1use bacnet_types::constructed::{
4 BACnetCalendarEntry, BACnetDateRange, BACnetObjectPropertyReference, BACnetSpecialEvent,
5 BACnetTimeValue,
6};
7use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10use std::borrow::Cow;
11
12use crate::common::read_property_list_property;
13use crate::traits::BACnetObject;
14
15pub struct CalendarObject {
25 oid: ObjectIdentifier,
26 name: String,
27 description: String,
28 present_value: bool,
29 status_flags: StatusFlags,
30 date_list: Vec<BACnetCalendarEntry>,
31}
32
33impl CalendarObject {
34 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
35 let oid = ObjectIdentifier::new(ObjectType::CALENDAR, instance)?;
36 Ok(Self {
37 oid,
38 name: name.into(),
39 description: String::new(),
40 present_value: false,
41 status_flags: StatusFlags::empty(),
42 date_list: Vec::new(),
43 })
44 }
45
46 pub fn set_present_value(&mut self, value: bool) {
48 self.present_value = value;
49 }
50
51 pub fn set_description(&mut self, desc: impl Into<String>) {
53 self.description = desc.into();
54 }
55
56 pub fn add_date_entry(&mut self, entry: BACnetCalendarEntry) {
58 self.date_list.push(entry);
59 }
60
61 pub fn clear_date_list(&mut self) {
63 self.date_list.clear();
64 }
65}
66
67impl BACnetObject for CalendarObject {
68 fn object_identifier(&self) -> ObjectIdentifier {
69 self.oid
70 }
71
72 fn object_name(&self) -> &str {
73 &self.name
74 }
75
76 fn read_property(
77 &self,
78 property: PropertyIdentifier,
79 array_index: Option<u32>,
80 ) -> Result<PropertyValue, Error> {
81 match property {
82 p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
83 Ok(PropertyValue::ObjectIdentifier(self.oid))
84 }
85 p if p == PropertyIdentifier::OBJECT_NAME => {
86 Ok(PropertyValue::CharacterString(self.name.clone()))
87 }
88 p if p == PropertyIdentifier::DESCRIPTION => {
89 Ok(PropertyValue::CharacterString(self.description.clone()))
90 }
91 p if p == PropertyIdentifier::OBJECT_TYPE => {
92 Ok(PropertyValue::Enumerated(ObjectType::CALENDAR.to_raw()))
93 }
94 p if p == PropertyIdentifier::PRESENT_VALUE => {
95 Ok(PropertyValue::Boolean(self.present_value))
96 }
97 p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
98 unused_bits: 4,
99 data: vec![self.status_flags.bits() << 4],
100 }),
101 p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
102 p if p == PropertyIdentifier::OUT_OF_SERVICE => Ok(PropertyValue::Boolean(false)),
103 p if p == PropertyIdentifier::DATE_LIST => Ok(PropertyValue::List(
104 self.date_list
105 .iter()
106 .map(|entry| match entry {
107 BACnetCalendarEntry::Date(d) => PropertyValue::Date(*d),
108 BACnetCalendarEntry::DateRange(dr) => {
109 PropertyValue::OctetString(dr.encode().to_vec())
110 }
111 BACnetCalendarEntry::WeekNDay(wnd) => {
112 PropertyValue::OctetString(wnd.encode().to_vec())
113 }
114 })
115 .collect(),
116 )),
117 p if p == PropertyIdentifier::PROPERTY_LIST => {
118 read_property_list_property(&self.property_list(), array_index)
119 }
120 _ => Err(Error::Protocol {
121 class: ErrorClass::PROPERTY.to_raw() as u32,
122 code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
123 }),
124 }
125 }
126
127 fn write_property(
128 &mut self,
129 property: PropertyIdentifier,
130 _array_index: Option<u32>,
131 value: PropertyValue,
132 _priority: Option<u8>,
133 ) -> Result<(), Error> {
134 if property == PropertyIdentifier::DESCRIPTION {
135 if let PropertyValue::CharacterString(s) = value {
136 self.description = s;
137 return Ok(());
138 }
139 return Err(Error::Protocol {
140 class: ErrorClass::PROPERTY.to_raw() as u32,
141 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
142 });
143 }
144 if property == PropertyIdentifier::PRESENT_VALUE {
145 return Err(Error::Protocol {
146 class: ErrorClass::PROPERTY.to_raw() as u32,
147 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
148 });
149 }
150 Err(Error::Protocol {
151 class: ErrorClass::PROPERTY.to_raw() as u32,
152 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
153 })
154 }
155
156 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
157 static PROPS: &[PropertyIdentifier] = &[
158 PropertyIdentifier::OBJECT_IDENTIFIER,
159 PropertyIdentifier::OBJECT_NAME,
160 PropertyIdentifier::DESCRIPTION,
161 PropertyIdentifier::OBJECT_TYPE,
162 PropertyIdentifier::PRESENT_VALUE,
163 PropertyIdentifier::DATE_LIST,
164 PropertyIdentifier::STATUS_FLAGS,
165 PropertyIdentifier::EVENT_STATE,
166 PropertyIdentifier::OUT_OF_SERVICE,
167 ];
168 Cow::Borrowed(PROPS)
169 }
170}
171
172pub struct ScheduleObject {
182 oid: ObjectIdentifier,
183 name: String,
184 description: String,
185 present_value: PropertyValue,
186 schedule_default: PropertyValue,
187 out_of_service: bool,
188 reliability: u32,
189 status_flags: StatusFlags,
190 weekly_schedule: [Vec<BACnetTimeValue>; 7],
192 exception_schedule: Vec<BACnetSpecialEvent>,
193 effective_period: Option<BACnetDateRange>,
194 list_of_object_property_references: Vec<BACnetObjectPropertyReference>,
195 priority_for_writing: u8,
197}
198
199impl ScheduleObject {
200 pub fn new(
201 instance: u32,
202 name: impl Into<String>,
203 schedule_default: PropertyValue,
204 ) -> Result<Self, Error> {
205 let oid = ObjectIdentifier::new(ObjectType::SCHEDULE, instance)?;
206 Ok(Self {
207 oid,
208 name: name.into(),
209 description: String::new(),
210 present_value: schedule_default.clone(),
211 schedule_default,
212 out_of_service: false,
213 reliability: 0,
214 status_flags: StatusFlags::empty(),
215 weekly_schedule: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
216 exception_schedule: Vec::new(),
217 effective_period: None,
218 list_of_object_property_references: Vec::new(),
219 priority_for_writing: 16, })
221 }
222
223 pub fn set_present_value(&mut self, value: PropertyValue) {
225 self.present_value = value;
226 }
227
228 pub fn set_description(&mut self, desc: impl Into<String>) {
230 self.description = desc.into();
231 }
232
233 pub fn set_weekly_schedule(&mut self, day_index: usize, entries: Vec<BACnetTimeValue>) {
235 if day_index < 7 {
236 self.weekly_schedule[day_index] = entries;
237 }
238 }
239
240 pub fn add_exception(&mut self, event: BACnetSpecialEvent) {
242 self.exception_schedule.push(event);
243 }
244
245 pub fn set_effective_period(&mut self, period: BACnetDateRange) {
247 self.effective_period = Some(period);
248 }
249
250 pub fn add_object_property_reference(&mut self, r: BACnetObjectPropertyReference) {
252 self.list_of_object_property_references.push(r);
253 }
254
255 pub fn present_value(&self) -> &PropertyValue {
257 &self.present_value
258 }
259
260 pub fn evaluate(&self, day_of_week: u8, hour: u8, minute: u8) -> PropertyValue {
265 if self.out_of_service {
266 return self.present_value.clone();
267 }
268
269 let mut best_exception: Option<(u8, &[u8])> = None;
271 for event in &self.exception_schedule {
272 if let Some(raw) = find_active_time_value(&event.list_of_time_values, hour, minute) {
273 match best_exception {
274 None => best_exception = Some((event.event_priority, raw)),
275 Some((p, _)) if event.event_priority < p => {
276 best_exception = Some((event.event_priority, raw));
277 }
278 _ => {}
279 }
280 }
281 }
282 if let Some((_, raw)) = best_exception {
283 return PropertyValue::OctetString(raw.to_vec());
284 }
285
286 if (day_of_week as usize) < 7 {
288 if let Some(raw) =
289 find_active_time_value(&self.weekly_schedule[day_of_week as usize], hour, minute)
290 {
291 return PropertyValue::OctetString(raw.to_vec());
292 }
293 }
294
295 self.schedule_default.clone()
297 }
298}
299
300fn find_active_time_value(entries: &[BACnetTimeValue], hour: u8, minute: u8) -> Option<&[u8]> {
304 let mut result = None;
305 for tv in entries {
306 let t = &tv.time;
307 if t.hour < hour || (t.hour == hour && t.minute <= minute) {
308 result = Some(tv.value.as_slice());
309 }
310 }
311 result
312}
313
314impl BACnetObject for ScheduleObject {
315 fn object_identifier(&self) -> ObjectIdentifier {
316 self.oid
317 }
318
319 fn object_name(&self) -> &str {
320 &self.name
321 }
322
323 fn read_property(
324 &self,
325 property: PropertyIdentifier,
326 array_index: Option<u32>,
327 ) -> Result<PropertyValue, Error> {
328 match property {
329 p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
330 Ok(PropertyValue::ObjectIdentifier(self.oid))
331 }
332 p if p == PropertyIdentifier::OBJECT_NAME => {
333 Ok(PropertyValue::CharacterString(self.name.clone()))
334 }
335 p if p == PropertyIdentifier::DESCRIPTION => {
336 Ok(PropertyValue::CharacterString(self.description.clone()))
337 }
338 p if p == PropertyIdentifier::OBJECT_TYPE => {
339 Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
340 }
341 p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
342 p if p == PropertyIdentifier::SCHEDULE_DEFAULT => Ok(self.schedule_default.clone()),
343 p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
344 unused_bits: 4,
345 data: vec![self.status_flags.bits() << 4],
346 }),
347 p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
348 p if p == PropertyIdentifier::RELIABILITY => {
349 Ok(PropertyValue::Enumerated(self.reliability))
350 }
351 p if p == PropertyIdentifier::OUT_OF_SERVICE => {
352 Ok(PropertyValue::Boolean(self.out_of_service))
353 }
354 p if p == PropertyIdentifier::WEEKLY_SCHEDULE => match array_index {
355 None => {
356 let days: Vec<PropertyValue> = self
357 .weekly_schedule
358 .iter()
359 .map(|day| {
360 PropertyValue::List(
361 day.iter()
362 .map(|tv| {
363 PropertyValue::List(vec![
364 PropertyValue::Time(tv.time),
365 PropertyValue::OctetString(tv.value.clone()),
366 ])
367 })
368 .collect(),
369 )
370 })
371 .collect();
372 Ok(PropertyValue::List(days))
373 }
374 Some(0) => Ok(PropertyValue::Unsigned(7)),
375 Some(idx) if (1..=7).contains(&idx) => {
376 let day = &self.weekly_schedule[(idx - 1) as usize];
377 Ok(PropertyValue::List(
378 day.iter()
379 .map(|tv| {
380 PropertyValue::List(vec![
381 PropertyValue::Time(tv.time),
382 PropertyValue::OctetString(tv.value.clone()),
383 ])
384 })
385 .collect(),
386 ))
387 }
388 _ => Err(Error::Protocol {
389 class: ErrorClass::PROPERTY.to_raw() as u32,
390 code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
391 }),
392 },
393 p if p == PropertyIdentifier::EXCEPTION_SCHEDULE => match array_index {
394 None => {
395 let events: Vec<PropertyValue> = self
396 .exception_schedule
397 .iter()
398 .map(|ev| {
399 let tvs: Vec<PropertyValue> = ev
400 .list_of_time_values
401 .iter()
402 .map(|tv| {
403 PropertyValue::List(vec![
404 PropertyValue::Time(tv.time),
405 PropertyValue::OctetString(tv.value.clone()),
406 ])
407 })
408 .collect();
409 PropertyValue::List(vec![
410 PropertyValue::Unsigned(ev.event_priority as u64),
411 PropertyValue::List(tvs),
412 ])
413 })
414 .collect();
415 Ok(PropertyValue::List(events))
416 }
417 Some(0) => Ok(PropertyValue::Unsigned(self.exception_schedule.len() as u64)),
418 Some(i) => {
419 let idx = (i as usize).checked_sub(1).ok_or(Error::Protocol {
420 class: ErrorClass::PROPERTY.to_raw() as u32,
421 code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
422 })?;
423 let ev = self.exception_schedule.get(idx).ok_or(Error::Protocol {
424 class: ErrorClass::PROPERTY.to_raw() as u32,
425 code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
426 })?;
427 let tvs: Vec<PropertyValue> = ev
428 .list_of_time_values
429 .iter()
430 .map(|tv| {
431 PropertyValue::List(vec![
432 PropertyValue::Time(tv.time),
433 PropertyValue::OctetString(tv.value.clone()),
434 ])
435 })
436 .collect();
437 Ok(PropertyValue::List(vec![
438 PropertyValue::Unsigned(ev.event_priority as u64),
439 PropertyValue::List(tvs),
440 ]))
441 }
442 },
443 p if p == PropertyIdentifier::EFFECTIVE_PERIOD => match &self.effective_period {
444 Some(dr) => Ok(PropertyValue::OctetString(dr.encode().to_vec())),
445 None => Ok(PropertyValue::Null),
446 },
447 p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => {
448 Ok(PropertyValue::List(
449 self.list_of_object_property_references
450 .iter()
451 .map(|r| {
452 PropertyValue::List(vec![
453 PropertyValue::ObjectIdentifier(r.object_identifier),
454 PropertyValue::Enumerated(r.property_identifier),
455 ])
456 })
457 .collect(),
458 ))
459 }
460 p if p == PropertyIdentifier::PRIORITY_FOR_WRITING => {
461 Ok(PropertyValue::Unsigned(self.priority_for_writing as u64))
462 }
463 p if p == PropertyIdentifier::PROPERTY_LIST => {
464 read_property_list_property(&self.property_list(), array_index)
465 }
466 _ => Err(Error::Protocol {
467 class: ErrorClass::PROPERTY.to_raw() as u32,
468 code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
469 }),
470 }
471 }
472
473 fn write_property(
474 &mut self,
475 property: PropertyIdentifier,
476 _array_index: Option<u32>,
477 value: PropertyValue,
478 _priority: Option<u8>,
479 ) -> Result<(), Error> {
480 if property == PropertyIdentifier::SCHEDULE_DEFAULT {
481 self.schedule_default = value;
482 return Ok(());
483 }
484 if property == PropertyIdentifier::RELIABILITY {
485 if let PropertyValue::Enumerated(v) = value {
486 self.reliability = v;
487 return Ok(());
488 }
489 return Err(Error::Protocol {
490 class: ErrorClass::PROPERTY.to_raw() as u32,
491 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
492 });
493 }
494 if property == PropertyIdentifier::OUT_OF_SERVICE {
495 if let PropertyValue::Boolean(v) = value {
496 self.out_of_service = v;
497 return Ok(());
498 }
499 return Err(Error::Protocol {
500 class: ErrorClass::PROPERTY.to_raw() as u32,
501 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
502 });
503 }
504 if property == PropertyIdentifier::DESCRIPTION {
505 if let PropertyValue::CharacterString(s) = value {
506 self.description = s;
507 return Ok(());
508 }
509 return Err(Error::Protocol {
510 class: ErrorClass::PROPERTY.to_raw() as u32,
511 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
512 });
513 }
514 Err(Error::Protocol {
515 class: ErrorClass::PROPERTY.to_raw() as u32,
516 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
517 })
518 }
519
520 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
521 static PROPS: &[PropertyIdentifier] = &[
522 PropertyIdentifier::OBJECT_IDENTIFIER,
523 PropertyIdentifier::OBJECT_NAME,
524 PropertyIdentifier::DESCRIPTION,
525 PropertyIdentifier::OBJECT_TYPE,
526 PropertyIdentifier::PRESENT_VALUE,
527 PropertyIdentifier::SCHEDULE_DEFAULT,
528 PropertyIdentifier::WEEKLY_SCHEDULE,
529 PropertyIdentifier::EXCEPTION_SCHEDULE,
530 PropertyIdentifier::EFFECTIVE_PERIOD,
531 PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
532 PropertyIdentifier::STATUS_FLAGS,
533 PropertyIdentifier::EVENT_STATE,
534 PropertyIdentifier::RELIABILITY,
535 PropertyIdentifier::OUT_OF_SERVICE,
536 ];
537 Cow::Borrowed(PROPS)
538 }
539
540 fn tick_schedule(
541 &mut self,
542 day_of_week: u8,
543 hour: u8,
544 minute: u8,
545 ) -> Option<(PropertyValue, Vec<(ObjectIdentifier, u32)>)> {
546 if self.out_of_service || self.list_of_object_property_references.is_empty() {
547 return None;
548 }
549
550 let new_value = self.evaluate(day_of_week, hour, minute);
551 if new_value == self.present_value {
552 return None;
553 }
554
555 self.present_value = new_value.clone();
556
557 let refs = self
558 .list_of_object_property_references
559 .iter()
560 .map(|r| (r.object_identifier, r.property_identifier))
561 .collect();
562
563 Some((new_value, refs))
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use bacnet_types::constructed::{
571 BACnetCalendarEntry, BACnetDateRange, BACnetSpecialEvent, BACnetTimeValue, BACnetWeekNDay,
572 SpecialEventPeriod,
573 };
574 use bacnet_types::primitives::{Date, Time};
575
576 #[test]
579 fn calendar_read_present_value_default() {
580 let cal = CalendarObject::new(1, "CAL-1").unwrap();
581 let val = cal
582 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
583 .unwrap();
584 assert_eq!(val, PropertyValue::Boolean(false));
585 }
586
587 #[test]
588 fn calendar_set_present_value() {
589 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
590 cal.set_present_value(true);
591 let val = cal
592 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
593 .unwrap();
594 assert_eq!(val, PropertyValue::Boolean(true));
595 }
596
597 #[test]
598 fn calendar_write_present_value_denied() {
599 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
600 let result = cal.write_property(
601 PropertyIdentifier::PRESENT_VALUE,
602 None,
603 PropertyValue::Boolean(true),
604 None,
605 );
606 assert!(result.is_err());
607 }
608
609 #[test]
610 fn calendar_date_list_empty_by_default() {
611 let cal = CalendarObject::new(1, "CAL-1").unwrap();
612 let val = cal
613 .read_property(PropertyIdentifier::DATE_LIST, None)
614 .unwrap();
615 assert_eq!(val, PropertyValue::List(vec![]));
616 }
617
618 #[test]
619 fn calendar_date_list_add_and_read_entries() {
620 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
621
622 let d = Date {
624 year: 124,
625 month: 3,
626 day: 15,
627 day_of_week: 5,
628 };
629 cal.add_date_entry(BACnetCalendarEntry::Date(d));
630
631 let dr = BACnetDateRange {
633 start_date: Date {
634 year: 124,
635 month: 6,
636 day: 1,
637 day_of_week: 6,
638 },
639 end_date: Date {
640 year: 124,
641 month: 6,
642 day: 30,
643 day_of_week: 0,
644 },
645 };
646 cal.add_date_entry(BACnetCalendarEntry::DateRange(dr.clone()));
647
648 let wnd = BACnetWeekNDay {
650 month: BACnetWeekNDay::ANY,
651 week_of_month: BACnetWeekNDay::ANY,
652 day_of_week: 1,
653 };
654 cal.add_date_entry(BACnetCalendarEntry::WeekNDay(wnd.clone()));
655
656 let val = cal
657 .read_property(PropertyIdentifier::DATE_LIST, None)
658 .unwrap();
659
660 if let PropertyValue::List(items) = val {
661 assert_eq!(items.len(), 3);
662 assert_eq!(items[0], PropertyValue::Date(d));
663 assert_eq!(items[1], PropertyValue::OctetString(dr.encode().to_vec()));
664 assert_eq!(items[2], PropertyValue::OctetString(wnd.encode().to_vec()));
665 } else {
666 panic!("expected PropertyValue::List");
667 }
668 }
669
670 #[test]
671 fn calendar_date_list_clear() {
672 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
673 let d = Date {
674 year: 124,
675 month: 1,
676 day: 1,
677 day_of_week: 1,
678 };
679 cal.add_date_entry(BACnetCalendarEntry::Date(d));
680 let val = cal
682 .read_property(PropertyIdentifier::DATE_LIST, None)
683 .unwrap();
684 if let PropertyValue::List(items) = &val {
685 assert_eq!(items.len(), 1);
686 } else {
687 panic!("expected PropertyValue::List");
688 }
689 cal.clear_date_list();
691 let val = cal
692 .read_property(PropertyIdentifier::DATE_LIST, None)
693 .unwrap();
694 assert_eq!(val, PropertyValue::List(vec![]));
695 }
696
697 #[test]
698 fn calendar_property_list_contains_date_list() {
699 let cal = CalendarObject::new(1, "CAL-1").unwrap();
700 let props = cal.property_list();
701 assert!(props.contains(&PropertyIdentifier::DATE_LIST));
702 }
703
704 #[test]
707 fn schedule_read_present_value_default() {
708 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
709 let val = sched
710 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
711 .unwrap();
712 assert_eq!(val, PropertyValue::Real(72.0));
713 }
714
715 #[test]
716 fn schedule_read_schedule_default() {
717 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
718 let val = sched
719 .read_property(PropertyIdentifier::SCHEDULE_DEFAULT, None)
720 .unwrap();
721 assert_eq!(val, PropertyValue::Real(72.0));
722 }
723
724 #[test]
725 fn schedule_write_schedule_default() {
726 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
727 sched
728 .write_property(
729 PropertyIdentifier::SCHEDULE_DEFAULT,
730 None,
731 PropertyValue::Real(68.0),
732 None,
733 )
734 .unwrap();
735 let val = sched
736 .read_property(PropertyIdentifier::SCHEDULE_DEFAULT, None)
737 .unwrap();
738 assert_eq!(val, PropertyValue::Real(68.0));
739 }
740
741 #[test]
742 fn schedule_set_present_value() {
743 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
744 sched.set_present_value(PropertyValue::Real(65.0));
745 let val = sched
746 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
747 .unwrap();
748 assert_eq!(val, PropertyValue::Real(65.0));
749 }
750
751 fn make_time(hour: u8, minute: u8) -> Time {
754 Time {
755 hour,
756 minute,
757 second: 0,
758 hundredths: 0,
759 }
760 }
761
762 fn make_tv(hour: u8, minute: u8, raw_value: Vec<u8>) -> BACnetTimeValue {
763 BACnetTimeValue {
764 time: make_time(hour, minute),
765 value: raw_value,
766 }
767 }
768
769 #[test]
770 fn schedule_weekly_schedule_empty_by_default() {
771 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
772 let val = sched
773 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, None)
774 .unwrap();
775 if let PropertyValue::List(days) = val {
776 assert_eq!(days.len(), 7);
777 for day in &days {
778 assert_eq!(*day, PropertyValue::List(vec![]));
779 }
780 } else {
781 panic!("expected PropertyValue::List");
782 }
783 }
784
785 #[test]
786 fn schedule_weekly_schedule_set_monday_read_no_index() {
787 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
788 let entries = vec![
789 make_tv(8, 0, vec![0x01]), make_tv(17, 0, vec![0x00]), ];
792 sched.set_weekly_schedule(0, entries.clone()); let val = sched
795 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, None)
796 .unwrap();
797 if let PropertyValue::List(days) = val {
798 assert_eq!(days.len(), 7);
799 if let PropertyValue::List(monday_entries) = &days[0] {
801 assert_eq!(monday_entries.len(), 2);
802 if let PropertyValue::List(pair) = &monday_entries[0] {
804 assert_eq!(pair[0], PropertyValue::Time(make_time(8, 0)));
805 assert_eq!(pair[1], PropertyValue::OctetString(vec![0x01]));
806 } else {
807 panic!("expected pair list");
808 }
809 } else {
810 panic!("expected Monday list");
811 }
812 for day in days.iter().skip(1) {
814 assert_eq!(*day, PropertyValue::List(vec![]));
815 }
816 } else {
817 panic!("expected PropertyValue::List");
818 }
819 }
820
821 #[test]
822 fn schedule_weekly_schedule_index_0_returns_count() {
823 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
824 let val = sched
825 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(0))
826 .unwrap();
827 assert_eq!(val, PropertyValue::Unsigned(7));
828 }
829
830 #[test]
831 fn schedule_weekly_schedule_index_1_returns_monday() {
832 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
833 let entries = vec![make_tv(9, 30, vec![0xAB])];
834 sched.set_weekly_schedule(0, entries); let val = sched
837 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(1))
838 .unwrap();
839 if let PropertyValue::List(items) = val {
840 assert_eq!(items.len(), 1);
841 if let PropertyValue::List(pair) = &items[0] {
842 assert_eq!(pair[0], PropertyValue::Time(make_time(9, 30)));
843 assert_eq!(pair[1], PropertyValue::OctetString(vec![0xAB]));
844 } else {
845 panic!("expected pair list");
846 }
847 } else {
848 panic!("expected PropertyValue::List");
849 }
850 }
851
852 #[test]
853 fn schedule_weekly_schedule_index_7_returns_sunday() {
854 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
855 let entries = vec![make_tv(10, 0, vec![0xFF])];
856 sched.set_weekly_schedule(6, entries); let val = sched
859 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(7))
860 .unwrap();
861 if let PropertyValue::List(items) = val {
862 assert_eq!(items.len(), 1);
863 } else {
864 panic!("expected PropertyValue::List");
865 }
866 }
867
868 #[test]
869 fn schedule_weekly_schedule_invalid_index_8_returns_error() {
870 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
871 let result = sched.read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(8));
872 assert!(result.is_err());
873 if let Err(Error::Protocol { code, .. }) = result {
874 assert_eq!(code, ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32);
875 } else {
876 panic!("expected Protocol error");
877 }
878 }
879
880 #[test]
881 fn schedule_weekly_schedule_out_of_bounds_day_index_ignored() {
882 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
883 sched.set_weekly_schedule(7, vec![make_tv(8, 0, vec![0x01])]);
885 let val = sched
887 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, None)
888 .unwrap();
889 if let PropertyValue::List(days) = val {
890 for day in &days {
891 assert_eq!(*day, PropertyValue::List(vec![]));
892 }
893 } else {
894 panic!("expected PropertyValue::List");
895 }
896 }
897
898 #[test]
901 fn schedule_effective_period_default_null() {
902 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
903 let val = sched
904 .read_property(PropertyIdentifier::EFFECTIVE_PERIOD, None)
905 .unwrap();
906 assert_eq!(val, PropertyValue::Null);
907 }
908
909 #[test]
910 fn schedule_effective_period_set_and_read() {
911 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
912 let period = BACnetDateRange {
913 start_date: Date {
914 year: 124,
915 month: 1,
916 day: 1,
917 day_of_week: 1,
918 },
919 end_date: Date {
920 year: 124,
921 month: 12,
922 day: 31,
923 day_of_week: 2,
924 },
925 };
926 sched.set_effective_period(period.clone());
927 let val = sched
928 .read_property(PropertyIdentifier::EFFECTIVE_PERIOD, None)
929 .unwrap();
930 assert_eq!(val, PropertyValue::OctetString(period.encode().to_vec()));
931 }
932
933 #[test]
936 fn schedule_exception_schedule_empty_by_default() {
937 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
938 let val = sched
939 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, None)
940 .unwrap();
941 assert_eq!(val, PropertyValue::List(vec![]));
942 }
943
944 #[test]
945 fn schedule_exception_schedule_count_via_index_zero() {
946 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
947 let event = BACnetSpecialEvent {
948 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
949 BACnetWeekNDay {
950 month: BACnetWeekNDay::ANY,
951 week_of_month: BACnetWeekNDay::ANY,
952 day_of_week: 7,
953 },
954 )),
955 list_of_time_values: vec![make_tv(0, 0, vec![0x00])],
956 event_priority: 16,
957 };
958 sched.add_exception(event);
959 let val = sched
960 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, Some(0))
961 .unwrap();
962 assert_eq!(val, PropertyValue::Unsigned(1));
963 }
964
965 #[test]
966 fn schedule_exception_schedule_add_and_read() {
967 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
968 let event = BACnetSpecialEvent {
969 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
970 BACnetWeekNDay {
971 month: BACnetWeekNDay::ANY,
972 week_of_month: BACnetWeekNDay::ANY,
973 day_of_week: 7, },
975 )),
976 list_of_time_values: vec![make_tv(0, 0, vec![0x00])],
977 event_priority: 16,
978 };
979 sched.add_exception(event);
980 let val = sched
981 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, None)
982 .unwrap();
983 if let PropertyValue::List(events) = &val {
985 assert_eq!(events.len(), 1);
986 } else {
987 panic!("expected List, got {val:?}");
988 }
989
990 let event2 = BACnetSpecialEvent {
992 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
993 BACnetWeekNDay {
994 month: BACnetWeekNDay::ANY,
995 week_of_month: BACnetWeekNDay::ANY,
996 day_of_week: 1, },
998 )),
999 list_of_time_values: vec![],
1000 event_priority: 14,
1001 };
1002 sched.add_exception(event2);
1003 let val = sched
1004 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, None)
1005 .unwrap();
1006 if let PropertyValue::List(events) = &val {
1007 assert_eq!(events.len(), 2);
1008 } else {
1009 panic!("expected List, got {val:?}");
1010 }
1011
1012 let count = sched
1014 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, Some(0))
1015 .unwrap();
1016 assert_eq!(count, PropertyValue::Unsigned(2));
1017 }
1018
1019 #[test]
1022 fn schedule_opr_list_empty_by_default() {
1023 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1024 let val = sched
1025 .read_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES, None)
1026 .unwrap();
1027 assert_eq!(val, PropertyValue::List(vec![]));
1028 }
1029
1030 #[test]
1031 fn schedule_opr_list_add_and_read() {
1032 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1033 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1034 let r = BACnetObjectPropertyReference::new(oid, PropertyIdentifier::PRESENT_VALUE.to_raw());
1035 sched.add_object_property_reference(r.clone());
1036
1037 let val = sched
1038 .read_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES, None)
1039 .unwrap();
1040 if let PropertyValue::List(items) = val {
1041 assert_eq!(items.len(), 1);
1042 if let PropertyValue::List(pair) = &items[0] {
1043 assert_eq!(pair[0], PropertyValue::ObjectIdentifier(oid));
1044 assert_eq!(
1045 pair[1],
1046 PropertyValue::Enumerated(PropertyIdentifier::PRESENT_VALUE.to_raw())
1047 );
1048 } else {
1049 panic!("expected pair list");
1050 }
1051 } else {
1052 panic!("expected PropertyValue::List");
1053 }
1054 }
1055
1056 #[test]
1057 fn schedule_opr_list_multiple_references() {
1058 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1059 let oid1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1060 let oid2 = ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, 5).unwrap();
1061 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1062 oid1,
1063 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1064 ));
1065 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1066 oid2,
1067 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1068 ));
1069
1070 let val = sched
1071 .read_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES, None)
1072 .unwrap();
1073 if let PropertyValue::List(items) = val {
1074 assert_eq!(items.len(), 2);
1075 } else {
1076 panic!("expected PropertyValue::List");
1077 }
1078 }
1079
1080 #[test]
1083 fn schedule_property_list_contains_new_properties() {
1084 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1085 let props = sched.property_list();
1086 assert!(props.contains(&PropertyIdentifier::WEEKLY_SCHEDULE));
1087 assert!(props.contains(&PropertyIdentifier::EXCEPTION_SCHEDULE));
1088 assert!(props.contains(&PropertyIdentifier::EFFECTIVE_PERIOD));
1089 assert!(props.contains(&PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES));
1090 }
1091
1092 #[test]
1095 fn evaluate_returns_default_when_no_entries() {
1096 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1097 let value = sched.evaluate(0, 12, 0); assert_eq!(value, PropertyValue::Real(72.0));
1099 }
1100
1101 #[test]
1102 fn evaluate_returns_weekly_value() {
1103 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1104 sched.set_weekly_schedule(
1106 0,
1107 vec![make_tv(8, 0, vec![0x01]), make_tv(17, 0, vec![0x00])],
1108 );
1109
1110 assert_eq!(sched.evaluate(0, 7, 59), PropertyValue::Real(72.0));
1112 assert_eq!(
1114 sched.evaluate(0, 8, 0),
1115 PropertyValue::OctetString(vec![0x01])
1116 );
1117 assert_eq!(
1119 sched.evaluate(0, 12, 0),
1120 PropertyValue::OctetString(vec![0x01])
1121 );
1122 assert_eq!(
1124 sched.evaluate(0, 17, 0),
1125 PropertyValue::OctetString(vec![0x00])
1126 );
1127 assert_eq!(
1129 sched.evaluate(0, 23, 59),
1130 PropertyValue::OctetString(vec![0x00])
1131 );
1132 }
1133
1134 #[test]
1135 fn evaluate_different_day_returns_default() {
1136 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1137 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1139
1140 assert_eq!(sched.evaluate(1, 12, 0), PropertyValue::Real(72.0));
1142 }
1143
1144 #[test]
1145 fn evaluate_exception_overrides_weekly() {
1146 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1147 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1149
1150 sched.add_exception(BACnetSpecialEvent {
1152 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1153 BACnetWeekNDay {
1154 month: BACnetWeekNDay::ANY,
1155 week_of_month: BACnetWeekNDay::ANY,
1156 day_of_week: BACnetWeekNDay::ANY,
1157 },
1158 )),
1159 list_of_time_values: vec![make_tv(0, 0, vec![0xFF])],
1160 event_priority: 10,
1161 });
1162
1163 assert_eq!(
1165 sched.evaluate(0, 12, 0),
1166 PropertyValue::OctetString(vec![0xFF])
1167 );
1168 }
1169
1170 #[test]
1171 fn evaluate_out_of_service_returns_present_value() {
1172 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1173 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1174 sched.set_present_value(PropertyValue::Real(55.0));
1175 sched.out_of_service = true;
1176
1177 assert_eq!(sched.evaluate(0, 12, 0), PropertyValue::Real(55.0));
1178 }
1179
1180 #[test]
1181 fn evaluate_exception_priority_lowest_number_wins() {
1182 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1183 sched.add_exception(BACnetSpecialEvent {
1185 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1186 BACnetWeekNDay {
1187 month: BACnetWeekNDay::ANY,
1188 week_of_month: BACnetWeekNDay::ANY,
1189 day_of_week: BACnetWeekNDay::ANY,
1190 },
1191 )),
1192 list_of_time_values: vec![make_tv(0, 0, vec![0xAA])],
1193 event_priority: 15,
1194 });
1195 sched.add_exception(BACnetSpecialEvent {
1196 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1197 BACnetWeekNDay {
1198 month: BACnetWeekNDay::ANY,
1199 week_of_month: BACnetWeekNDay::ANY,
1200 day_of_week: BACnetWeekNDay::ANY,
1201 },
1202 )),
1203 list_of_time_values: vec![make_tv(0, 0, vec![0xBB])],
1204 event_priority: 5,
1205 });
1206
1207 assert_eq!(
1209 sched.evaluate(0, 12, 0),
1210 PropertyValue::OctetString(vec![0xBB])
1211 );
1212 }
1213
1214 #[test]
1217 fn tick_schedule_returns_none_when_no_refs() {
1218 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1219 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1220 assert!(sched.tick_schedule(0, 12, 0).is_none());
1222 }
1223
1224 #[test]
1225 fn tick_schedule_returns_none_when_value_unchanged() {
1226 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1227 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1228 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1229 oid,
1230 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1231 ));
1232 assert!(sched.tick_schedule(0, 12, 0).is_none());
1234 }
1235
1236 #[test]
1237 fn tick_schedule_returns_value_and_refs_on_change() {
1238 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1239 let target_oid = ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 5).unwrap();
1240 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1241 target_oid,
1242 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1243 ));
1244 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1245
1246 let result = sched.tick_schedule(0, 12, 0);
1247 assert!(result.is_some());
1248 let (value, refs) = result.unwrap();
1249 assert_eq!(value, PropertyValue::OctetString(vec![0x01]));
1250 assert_eq!(refs.len(), 1);
1251 assert_eq!(refs[0].0, target_oid);
1252 assert_eq!(refs[0].1, PropertyIdentifier::PRESENT_VALUE.to_raw());
1253 }
1254
1255 #[test]
1256 fn tick_schedule_updates_present_value() {
1257 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1258 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1259 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1260 oid,
1261 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1262 ));
1263 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1264
1265 let _ = sched.tick_schedule(0, 12, 0);
1266 assert_eq!(
1267 *sched.present_value(),
1268 PropertyValue::OctetString(vec![0x01])
1269 );
1270
1271 assert!(sched.tick_schedule(0, 12, 0).is_none());
1273 }
1274
1275 #[test]
1276 fn tick_schedule_returns_none_when_out_of_service() {
1277 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1278 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1279 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1280 oid,
1281 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1282 ));
1283 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1284 sched.out_of_service = true;
1285
1286 assert!(sched.tick_schedule(0, 12, 0).is_none());
1287 }
1288}