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}
196
197impl ScheduleObject {
198 pub fn new(
199 instance: u32,
200 name: impl Into<String>,
201 schedule_default: PropertyValue,
202 ) -> Result<Self, Error> {
203 let oid = ObjectIdentifier::new(ObjectType::SCHEDULE, instance)?;
204 Ok(Self {
205 oid,
206 name: name.into(),
207 description: String::new(),
208 present_value: schedule_default.clone(),
209 schedule_default,
210 out_of_service: false,
211 reliability: 0,
212 status_flags: StatusFlags::empty(),
213 weekly_schedule: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
214 exception_schedule: Vec::new(),
215 effective_period: None,
216 list_of_object_property_references: Vec::new(),
217 })
218 }
219
220 pub fn set_present_value(&mut self, value: PropertyValue) {
222 self.present_value = value;
223 }
224
225 pub fn set_description(&mut self, desc: impl Into<String>) {
227 self.description = desc.into();
228 }
229
230 pub fn set_weekly_schedule(&mut self, day_index: usize, entries: Vec<BACnetTimeValue>) {
232 if day_index < 7 {
233 self.weekly_schedule[day_index] = entries;
234 }
235 }
236
237 pub fn add_exception(&mut self, event: BACnetSpecialEvent) {
239 self.exception_schedule.push(event);
240 }
241
242 pub fn set_effective_period(&mut self, period: BACnetDateRange) {
244 self.effective_period = Some(period);
245 }
246
247 pub fn add_object_property_reference(&mut self, r: BACnetObjectPropertyReference) {
249 self.list_of_object_property_references.push(r);
250 }
251
252 pub fn present_value(&self) -> &PropertyValue {
254 &self.present_value
255 }
256
257 pub fn evaluate(&self, day_of_week: u8, hour: u8, minute: u8) -> PropertyValue {
262 if self.out_of_service {
263 return self.present_value.clone();
264 }
265
266 let mut best_exception: Option<(u8, &[u8])> = None;
268 for event in &self.exception_schedule {
269 if let Some(raw) = find_active_time_value(&event.list_of_time_values, hour, minute) {
270 match best_exception {
271 None => best_exception = Some((event.event_priority, raw)),
272 Some((p, _)) if event.event_priority < p => {
273 best_exception = Some((event.event_priority, raw));
274 }
275 _ => {}
276 }
277 }
278 }
279 if let Some((_, raw)) = best_exception {
280 return PropertyValue::OctetString(raw.to_vec());
281 }
282
283 if (day_of_week as usize) < 7 {
285 if let Some(raw) =
286 find_active_time_value(&self.weekly_schedule[day_of_week as usize], hour, minute)
287 {
288 return PropertyValue::OctetString(raw.to_vec());
289 }
290 }
291
292 self.schedule_default.clone()
294 }
295}
296
297fn find_active_time_value(entries: &[BACnetTimeValue], hour: u8, minute: u8) -> Option<&[u8]> {
301 let mut result = None;
302 for tv in entries {
303 let t = &tv.time;
304 if t.hour < hour || (t.hour == hour && t.minute <= minute) {
305 result = Some(tv.value.as_slice());
306 }
307 }
308 result
309}
310
311impl BACnetObject for ScheduleObject {
312 fn object_identifier(&self) -> ObjectIdentifier {
313 self.oid
314 }
315
316 fn object_name(&self) -> &str {
317 &self.name
318 }
319
320 fn read_property(
321 &self,
322 property: PropertyIdentifier,
323 array_index: Option<u32>,
324 ) -> Result<PropertyValue, Error> {
325 match property {
326 p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
327 Ok(PropertyValue::ObjectIdentifier(self.oid))
328 }
329 p if p == PropertyIdentifier::OBJECT_NAME => {
330 Ok(PropertyValue::CharacterString(self.name.clone()))
331 }
332 p if p == PropertyIdentifier::DESCRIPTION => {
333 Ok(PropertyValue::CharacterString(self.description.clone()))
334 }
335 p if p == PropertyIdentifier::OBJECT_TYPE => {
336 Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
337 }
338 p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
339 p if p == PropertyIdentifier::SCHEDULE_DEFAULT => Ok(self.schedule_default.clone()),
340 p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
341 unused_bits: 4,
342 data: vec![self.status_flags.bits() << 4],
343 }),
344 p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
345 p if p == PropertyIdentifier::RELIABILITY => {
346 Ok(PropertyValue::Enumerated(self.reliability))
347 }
348 p if p == PropertyIdentifier::OUT_OF_SERVICE => {
349 Ok(PropertyValue::Boolean(self.out_of_service))
350 }
351 p if p == PropertyIdentifier::WEEKLY_SCHEDULE => match array_index {
352 None => {
353 let days: Vec<PropertyValue> = self
354 .weekly_schedule
355 .iter()
356 .map(|day| {
357 PropertyValue::List(
358 day.iter()
359 .map(|tv| {
360 PropertyValue::List(vec![
361 PropertyValue::Time(tv.time),
362 PropertyValue::OctetString(tv.value.clone()),
363 ])
364 })
365 .collect(),
366 )
367 })
368 .collect();
369 Ok(PropertyValue::List(days))
370 }
371 Some(0) => Ok(PropertyValue::Unsigned(7)),
372 Some(idx) if (1..=7).contains(&idx) => {
373 let day = &self.weekly_schedule[(idx - 1) as usize];
374 Ok(PropertyValue::List(
375 day.iter()
376 .map(|tv| {
377 PropertyValue::List(vec![
378 PropertyValue::Time(tv.time),
379 PropertyValue::OctetString(tv.value.clone()),
380 ])
381 })
382 .collect(),
383 ))
384 }
385 _ => Err(Error::Protocol {
386 class: ErrorClass::PROPERTY.to_raw() as u32,
387 code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
388 }),
389 },
390 p if p == PropertyIdentifier::EXCEPTION_SCHEDULE => match array_index {
391 None => {
392 let events: Vec<PropertyValue> = self
393 .exception_schedule
394 .iter()
395 .map(|ev| {
396 let tvs: Vec<PropertyValue> = ev
397 .list_of_time_values
398 .iter()
399 .map(|tv| {
400 PropertyValue::List(vec![
401 PropertyValue::Time(tv.time),
402 PropertyValue::OctetString(tv.value.clone()),
403 ])
404 })
405 .collect();
406 PropertyValue::List(vec![
407 PropertyValue::Unsigned(ev.event_priority as u64),
408 PropertyValue::List(tvs),
409 ])
410 })
411 .collect();
412 Ok(PropertyValue::List(events))
413 }
414 Some(0) => Ok(PropertyValue::Unsigned(self.exception_schedule.len() as u64)),
415 Some(i) => {
416 let idx = (i as usize).checked_sub(1).ok_or(Error::Protocol {
417 class: ErrorClass::PROPERTY.to_raw() as u32,
418 code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
419 })?;
420 let ev = self.exception_schedule.get(idx).ok_or(Error::Protocol {
421 class: ErrorClass::PROPERTY.to_raw() as u32,
422 code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
423 })?;
424 let tvs: Vec<PropertyValue> = ev
425 .list_of_time_values
426 .iter()
427 .map(|tv| {
428 PropertyValue::List(vec![
429 PropertyValue::Time(tv.time),
430 PropertyValue::OctetString(tv.value.clone()),
431 ])
432 })
433 .collect();
434 Ok(PropertyValue::List(vec![
435 PropertyValue::Unsigned(ev.event_priority as u64),
436 PropertyValue::List(tvs),
437 ]))
438 }
439 },
440 p if p == PropertyIdentifier::EFFECTIVE_PERIOD => match &self.effective_period {
441 Some(dr) => Ok(PropertyValue::OctetString(dr.encode().to_vec())),
442 None => Ok(PropertyValue::Null),
443 },
444 p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => {
445 Ok(PropertyValue::List(
446 self.list_of_object_property_references
447 .iter()
448 .map(|r| {
449 PropertyValue::List(vec![
450 PropertyValue::ObjectIdentifier(r.object_identifier),
451 PropertyValue::Enumerated(r.property_identifier),
452 ])
453 })
454 .collect(),
455 ))
456 }
457 p if p == PropertyIdentifier::PROPERTY_LIST => {
458 read_property_list_property(&self.property_list(), array_index)
459 }
460 _ => Err(Error::Protocol {
461 class: ErrorClass::PROPERTY.to_raw() as u32,
462 code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
463 }),
464 }
465 }
466
467 fn write_property(
468 &mut self,
469 property: PropertyIdentifier,
470 _array_index: Option<u32>,
471 value: PropertyValue,
472 _priority: Option<u8>,
473 ) -> Result<(), Error> {
474 if property == PropertyIdentifier::SCHEDULE_DEFAULT {
475 self.schedule_default = value;
476 return Ok(());
477 }
478 if property == PropertyIdentifier::RELIABILITY {
479 if let PropertyValue::Enumerated(v) = value {
480 self.reliability = v;
481 return Ok(());
482 }
483 return Err(Error::Protocol {
484 class: ErrorClass::PROPERTY.to_raw() as u32,
485 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
486 });
487 }
488 if property == PropertyIdentifier::OUT_OF_SERVICE {
489 if let PropertyValue::Boolean(v) = value {
490 self.out_of_service = v;
491 return Ok(());
492 }
493 return Err(Error::Protocol {
494 class: ErrorClass::PROPERTY.to_raw() as u32,
495 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
496 });
497 }
498 if property == PropertyIdentifier::DESCRIPTION {
499 if let PropertyValue::CharacterString(s) = value {
500 self.description = s;
501 return Ok(());
502 }
503 return Err(Error::Protocol {
504 class: ErrorClass::PROPERTY.to_raw() as u32,
505 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
506 });
507 }
508 Err(Error::Protocol {
509 class: ErrorClass::PROPERTY.to_raw() as u32,
510 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
511 })
512 }
513
514 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
515 static PROPS: &[PropertyIdentifier] = &[
516 PropertyIdentifier::OBJECT_IDENTIFIER,
517 PropertyIdentifier::OBJECT_NAME,
518 PropertyIdentifier::DESCRIPTION,
519 PropertyIdentifier::OBJECT_TYPE,
520 PropertyIdentifier::PRESENT_VALUE,
521 PropertyIdentifier::SCHEDULE_DEFAULT,
522 PropertyIdentifier::WEEKLY_SCHEDULE,
523 PropertyIdentifier::EXCEPTION_SCHEDULE,
524 PropertyIdentifier::EFFECTIVE_PERIOD,
525 PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
526 PropertyIdentifier::STATUS_FLAGS,
527 PropertyIdentifier::EVENT_STATE,
528 PropertyIdentifier::RELIABILITY,
529 PropertyIdentifier::OUT_OF_SERVICE,
530 ];
531 Cow::Borrowed(PROPS)
532 }
533
534 fn tick_schedule(
535 &mut self,
536 day_of_week: u8,
537 hour: u8,
538 minute: u8,
539 ) -> Option<(PropertyValue, Vec<(ObjectIdentifier, u32)>)> {
540 if self.out_of_service || self.list_of_object_property_references.is_empty() {
541 return None;
542 }
543
544 let new_value = self.evaluate(day_of_week, hour, minute);
545 if new_value == self.present_value {
546 return None;
547 }
548
549 self.present_value = new_value.clone();
550
551 let refs = self
552 .list_of_object_property_references
553 .iter()
554 .map(|r| (r.object_identifier, r.property_identifier))
555 .collect();
556
557 Some((new_value, refs))
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use bacnet_types::constructed::{
565 BACnetCalendarEntry, BACnetDateRange, BACnetSpecialEvent, BACnetTimeValue, BACnetWeekNDay,
566 SpecialEventPeriod,
567 };
568 use bacnet_types::primitives::{Date, Time};
569
570 #[test]
573 fn calendar_read_present_value_default() {
574 let cal = CalendarObject::new(1, "CAL-1").unwrap();
575 let val = cal
576 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
577 .unwrap();
578 assert_eq!(val, PropertyValue::Boolean(false));
579 }
580
581 #[test]
582 fn calendar_set_present_value() {
583 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
584 cal.set_present_value(true);
585 let val = cal
586 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
587 .unwrap();
588 assert_eq!(val, PropertyValue::Boolean(true));
589 }
590
591 #[test]
592 fn calendar_write_present_value_denied() {
593 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
594 let result = cal.write_property(
595 PropertyIdentifier::PRESENT_VALUE,
596 None,
597 PropertyValue::Boolean(true),
598 None,
599 );
600 assert!(result.is_err());
601 }
602
603 #[test]
604 fn calendar_date_list_empty_by_default() {
605 let cal = CalendarObject::new(1, "CAL-1").unwrap();
606 let val = cal
607 .read_property(PropertyIdentifier::DATE_LIST, None)
608 .unwrap();
609 assert_eq!(val, PropertyValue::List(vec![]));
610 }
611
612 #[test]
613 fn calendar_date_list_add_and_read_entries() {
614 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
615
616 let d = Date {
618 year: 124,
619 month: 3,
620 day: 15,
621 day_of_week: 5,
622 };
623 cal.add_date_entry(BACnetCalendarEntry::Date(d));
624
625 let dr = BACnetDateRange {
627 start_date: Date {
628 year: 124,
629 month: 6,
630 day: 1,
631 day_of_week: 6,
632 },
633 end_date: Date {
634 year: 124,
635 month: 6,
636 day: 30,
637 day_of_week: 0,
638 },
639 };
640 cal.add_date_entry(BACnetCalendarEntry::DateRange(dr.clone()));
641
642 let wnd = BACnetWeekNDay {
644 month: BACnetWeekNDay::ANY,
645 week_of_month: BACnetWeekNDay::ANY,
646 day_of_week: 1,
647 };
648 cal.add_date_entry(BACnetCalendarEntry::WeekNDay(wnd.clone()));
649
650 let val = cal
651 .read_property(PropertyIdentifier::DATE_LIST, None)
652 .unwrap();
653
654 if let PropertyValue::List(items) = val {
655 assert_eq!(items.len(), 3);
656 assert_eq!(items[0], PropertyValue::Date(d));
657 assert_eq!(items[1], PropertyValue::OctetString(dr.encode().to_vec()));
658 assert_eq!(items[2], PropertyValue::OctetString(wnd.encode().to_vec()));
659 } else {
660 panic!("expected PropertyValue::List");
661 }
662 }
663
664 #[test]
665 fn calendar_date_list_clear() {
666 let mut cal = CalendarObject::new(1, "CAL-1").unwrap();
667 let d = Date {
668 year: 124,
669 month: 1,
670 day: 1,
671 day_of_week: 1,
672 };
673 cal.add_date_entry(BACnetCalendarEntry::Date(d));
674 let val = cal
676 .read_property(PropertyIdentifier::DATE_LIST, None)
677 .unwrap();
678 if let PropertyValue::List(items) = &val {
679 assert_eq!(items.len(), 1);
680 } else {
681 panic!("expected PropertyValue::List");
682 }
683 cal.clear_date_list();
685 let val = cal
686 .read_property(PropertyIdentifier::DATE_LIST, None)
687 .unwrap();
688 assert_eq!(val, PropertyValue::List(vec![]));
689 }
690
691 #[test]
692 fn calendar_property_list_contains_date_list() {
693 let cal = CalendarObject::new(1, "CAL-1").unwrap();
694 let props = cal.property_list();
695 assert!(props.contains(&PropertyIdentifier::DATE_LIST));
696 }
697
698 #[test]
701 fn schedule_read_present_value_default() {
702 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
703 let val = sched
704 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
705 .unwrap();
706 assert_eq!(val, PropertyValue::Real(72.0));
707 }
708
709 #[test]
710 fn schedule_read_schedule_default() {
711 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
712 let val = sched
713 .read_property(PropertyIdentifier::SCHEDULE_DEFAULT, None)
714 .unwrap();
715 assert_eq!(val, PropertyValue::Real(72.0));
716 }
717
718 #[test]
719 fn schedule_write_schedule_default() {
720 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
721 sched
722 .write_property(
723 PropertyIdentifier::SCHEDULE_DEFAULT,
724 None,
725 PropertyValue::Real(68.0),
726 None,
727 )
728 .unwrap();
729 let val = sched
730 .read_property(PropertyIdentifier::SCHEDULE_DEFAULT, None)
731 .unwrap();
732 assert_eq!(val, PropertyValue::Real(68.0));
733 }
734
735 #[test]
736 fn schedule_set_present_value() {
737 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
738 sched.set_present_value(PropertyValue::Real(65.0));
739 let val = sched
740 .read_property(PropertyIdentifier::PRESENT_VALUE, None)
741 .unwrap();
742 assert_eq!(val, PropertyValue::Real(65.0));
743 }
744
745 fn make_time(hour: u8, minute: u8) -> Time {
748 Time {
749 hour,
750 minute,
751 second: 0,
752 hundredths: 0,
753 }
754 }
755
756 fn make_tv(hour: u8, minute: u8, raw_value: Vec<u8>) -> BACnetTimeValue {
757 BACnetTimeValue {
758 time: make_time(hour, minute),
759 value: raw_value,
760 }
761 }
762
763 #[test]
764 fn schedule_weekly_schedule_empty_by_default() {
765 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
766 let val = sched
767 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, None)
768 .unwrap();
769 if let PropertyValue::List(days) = val {
770 assert_eq!(days.len(), 7);
771 for day in &days {
772 assert_eq!(*day, PropertyValue::List(vec![]));
773 }
774 } else {
775 panic!("expected PropertyValue::List");
776 }
777 }
778
779 #[test]
780 fn schedule_weekly_schedule_set_monday_read_no_index() {
781 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
782 let entries = vec![
783 make_tv(8, 0, vec![0x01]), make_tv(17, 0, vec![0x00]), ];
786 sched.set_weekly_schedule(0, entries.clone()); let val = sched
789 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, None)
790 .unwrap();
791 if let PropertyValue::List(days) = val {
792 assert_eq!(days.len(), 7);
793 if let PropertyValue::List(monday_entries) = &days[0] {
795 assert_eq!(monday_entries.len(), 2);
796 if let PropertyValue::List(pair) = &monday_entries[0] {
798 assert_eq!(pair[0], PropertyValue::Time(make_time(8, 0)));
799 assert_eq!(pair[1], PropertyValue::OctetString(vec![0x01]));
800 } else {
801 panic!("expected pair list");
802 }
803 } else {
804 panic!("expected Monday list");
805 }
806 for day in days.iter().skip(1) {
808 assert_eq!(*day, PropertyValue::List(vec![]));
809 }
810 } else {
811 panic!("expected PropertyValue::List");
812 }
813 }
814
815 #[test]
816 fn schedule_weekly_schedule_index_0_returns_count() {
817 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
818 let val = sched
819 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(0))
820 .unwrap();
821 assert_eq!(val, PropertyValue::Unsigned(7));
822 }
823
824 #[test]
825 fn schedule_weekly_schedule_index_1_returns_monday() {
826 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
827 let entries = vec![make_tv(9, 30, vec![0xAB])];
828 sched.set_weekly_schedule(0, entries); let val = sched
831 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(1))
832 .unwrap();
833 if let PropertyValue::List(items) = val {
834 assert_eq!(items.len(), 1);
835 if let PropertyValue::List(pair) = &items[0] {
836 assert_eq!(pair[0], PropertyValue::Time(make_time(9, 30)));
837 assert_eq!(pair[1], PropertyValue::OctetString(vec![0xAB]));
838 } else {
839 panic!("expected pair list");
840 }
841 } else {
842 panic!("expected PropertyValue::List");
843 }
844 }
845
846 #[test]
847 fn schedule_weekly_schedule_index_7_returns_sunday() {
848 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
849 let entries = vec![make_tv(10, 0, vec![0xFF])];
850 sched.set_weekly_schedule(6, entries); let val = sched
853 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(7))
854 .unwrap();
855 if let PropertyValue::List(items) = val {
856 assert_eq!(items.len(), 1);
857 } else {
858 panic!("expected PropertyValue::List");
859 }
860 }
861
862 #[test]
863 fn schedule_weekly_schedule_invalid_index_8_returns_error() {
864 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
865 let result = sched.read_property(PropertyIdentifier::WEEKLY_SCHEDULE, Some(8));
866 assert!(result.is_err());
867 if let Err(Error::Protocol { code, .. }) = result {
868 assert_eq!(code, ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32);
869 } else {
870 panic!("expected Protocol error");
871 }
872 }
873
874 #[test]
875 fn schedule_weekly_schedule_out_of_bounds_day_index_ignored() {
876 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
877 sched.set_weekly_schedule(7, vec![make_tv(8, 0, vec![0x01])]);
879 let val = sched
881 .read_property(PropertyIdentifier::WEEKLY_SCHEDULE, None)
882 .unwrap();
883 if let PropertyValue::List(days) = val {
884 for day in &days {
885 assert_eq!(*day, PropertyValue::List(vec![]));
886 }
887 } else {
888 panic!("expected PropertyValue::List");
889 }
890 }
891
892 #[test]
895 fn schedule_effective_period_default_null() {
896 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
897 let val = sched
898 .read_property(PropertyIdentifier::EFFECTIVE_PERIOD, None)
899 .unwrap();
900 assert_eq!(val, PropertyValue::Null);
901 }
902
903 #[test]
904 fn schedule_effective_period_set_and_read() {
905 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
906 let period = BACnetDateRange {
907 start_date: Date {
908 year: 124,
909 month: 1,
910 day: 1,
911 day_of_week: 1,
912 },
913 end_date: Date {
914 year: 124,
915 month: 12,
916 day: 31,
917 day_of_week: 2,
918 },
919 };
920 sched.set_effective_period(period.clone());
921 let val = sched
922 .read_property(PropertyIdentifier::EFFECTIVE_PERIOD, None)
923 .unwrap();
924 assert_eq!(val, PropertyValue::OctetString(period.encode().to_vec()));
925 }
926
927 #[test]
930 fn schedule_exception_schedule_empty_by_default() {
931 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
932 let val = sched
933 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, None)
934 .unwrap();
935 assert_eq!(val, PropertyValue::List(vec![]));
936 }
937
938 #[test]
939 fn schedule_exception_schedule_count_via_index_zero() {
940 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
941 let event = BACnetSpecialEvent {
942 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
943 BACnetWeekNDay {
944 month: BACnetWeekNDay::ANY,
945 week_of_month: BACnetWeekNDay::ANY,
946 day_of_week: 7,
947 },
948 )),
949 list_of_time_values: vec![make_tv(0, 0, vec![0x00])],
950 event_priority: 16,
951 };
952 sched.add_exception(event);
953 let val = sched
954 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, Some(0))
955 .unwrap();
956 assert_eq!(val, PropertyValue::Unsigned(1));
957 }
958
959 #[test]
960 fn schedule_exception_schedule_add_and_read() {
961 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
962 let event = BACnetSpecialEvent {
963 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
964 BACnetWeekNDay {
965 month: BACnetWeekNDay::ANY,
966 week_of_month: BACnetWeekNDay::ANY,
967 day_of_week: 7, },
969 )),
970 list_of_time_values: vec![make_tv(0, 0, vec![0x00])],
971 event_priority: 16,
972 };
973 sched.add_exception(event);
974 let val = sched
975 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, None)
976 .unwrap();
977 if let PropertyValue::List(events) = &val {
979 assert_eq!(events.len(), 1);
980 } else {
981 panic!("expected List, got {val:?}");
982 }
983
984 let event2 = BACnetSpecialEvent {
986 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
987 BACnetWeekNDay {
988 month: BACnetWeekNDay::ANY,
989 week_of_month: BACnetWeekNDay::ANY,
990 day_of_week: 1, },
992 )),
993 list_of_time_values: vec![],
994 event_priority: 14,
995 };
996 sched.add_exception(event2);
997 let val = sched
998 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, None)
999 .unwrap();
1000 if let PropertyValue::List(events) = &val {
1001 assert_eq!(events.len(), 2);
1002 } else {
1003 panic!("expected List, got {val:?}");
1004 }
1005
1006 let count = sched
1008 .read_property(PropertyIdentifier::EXCEPTION_SCHEDULE, Some(0))
1009 .unwrap();
1010 assert_eq!(count, PropertyValue::Unsigned(2));
1011 }
1012
1013 #[test]
1016 fn schedule_opr_list_empty_by_default() {
1017 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1018 let val = sched
1019 .read_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES, None)
1020 .unwrap();
1021 assert_eq!(val, PropertyValue::List(vec![]));
1022 }
1023
1024 #[test]
1025 fn schedule_opr_list_add_and_read() {
1026 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1027 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1028 let r = BACnetObjectPropertyReference::new(oid, PropertyIdentifier::PRESENT_VALUE.to_raw());
1029 sched.add_object_property_reference(r.clone());
1030
1031 let val = sched
1032 .read_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES, None)
1033 .unwrap();
1034 if let PropertyValue::List(items) = val {
1035 assert_eq!(items.len(), 1);
1036 if let PropertyValue::List(pair) = &items[0] {
1037 assert_eq!(pair[0], PropertyValue::ObjectIdentifier(oid));
1038 assert_eq!(
1039 pair[1],
1040 PropertyValue::Enumerated(PropertyIdentifier::PRESENT_VALUE.to_raw())
1041 );
1042 } else {
1043 panic!("expected pair list");
1044 }
1045 } else {
1046 panic!("expected PropertyValue::List");
1047 }
1048 }
1049
1050 #[test]
1051 fn schedule_opr_list_multiple_references() {
1052 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1053 let oid1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1054 let oid2 = ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, 5).unwrap();
1055 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1056 oid1,
1057 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1058 ));
1059 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1060 oid2,
1061 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1062 ));
1063
1064 let val = sched
1065 .read_property(PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES, None)
1066 .unwrap();
1067 if let PropertyValue::List(items) = val {
1068 assert_eq!(items.len(), 2);
1069 } else {
1070 panic!("expected PropertyValue::List");
1071 }
1072 }
1073
1074 #[test]
1077 fn schedule_property_list_contains_new_properties() {
1078 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1079 let props = sched.property_list();
1080 assert!(props.contains(&PropertyIdentifier::WEEKLY_SCHEDULE));
1081 assert!(props.contains(&PropertyIdentifier::EXCEPTION_SCHEDULE));
1082 assert!(props.contains(&PropertyIdentifier::EFFECTIVE_PERIOD));
1083 assert!(props.contains(&PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES));
1084 }
1085
1086 #[test]
1089 fn evaluate_returns_default_when_no_entries() {
1090 let sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1091 let value = sched.evaluate(0, 12, 0); assert_eq!(value, PropertyValue::Real(72.0));
1093 }
1094
1095 #[test]
1096 fn evaluate_returns_weekly_value() {
1097 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1098 sched.set_weekly_schedule(
1100 0,
1101 vec![make_tv(8, 0, vec![0x01]), make_tv(17, 0, vec![0x00])],
1102 );
1103
1104 assert_eq!(sched.evaluate(0, 7, 59), PropertyValue::Real(72.0));
1106 assert_eq!(
1108 sched.evaluate(0, 8, 0),
1109 PropertyValue::OctetString(vec![0x01])
1110 );
1111 assert_eq!(
1113 sched.evaluate(0, 12, 0),
1114 PropertyValue::OctetString(vec![0x01])
1115 );
1116 assert_eq!(
1118 sched.evaluate(0, 17, 0),
1119 PropertyValue::OctetString(vec![0x00])
1120 );
1121 assert_eq!(
1123 sched.evaluate(0, 23, 59),
1124 PropertyValue::OctetString(vec![0x00])
1125 );
1126 }
1127
1128 #[test]
1129 fn evaluate_different_day_returns_default() {
1130 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1131 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1133
1134 assert_eq!(sched.evaluate(1, 12, 0), PropertyValue::Real(72.0));
1136 }
1137
1138 #[test]
1139 fn evaluate_exception_overrides_weekly() {
1140 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1141 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1143
1144 sched.add_exception(BACnetSpecialEvent {
1146 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1147 BACnetWeekNDay {
1148 month: BACnetWeekNDay::ANY,
1149 week_of_month: BACnetWeekNDay::ANY,
1150 day_of_week: BACnetWeekNDay::ANY,
1151 },
1152 )),
1153 list_of_time_values: vec![make_tv(0, 0, vec![0xFF])],
1154 event_priority: 10,
1155 });
1156
1157 assert_eq!(
1159 sched.evaluate(0, 12, 0),
1160 PropertyValue::OctetString(vec![0xFF])
1161 );
1162 }
1163
1164 #[test]
1165 fn evaluate_out_of_service_returns_present_value() {
1166 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1167 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1168 sched.set_present_value(PropertyValue::Real(55.0));
1169 sched.out_of_service = true;
1170
1171 assert_eq!(sched.evaluate(0, 12, 0), PropertyValue::Real(55.0));
1172 }
1173
1174 #[test]
1175 fn evaluate_exception_priority_lowest_number_wins() {
1176 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1177 sched.add_exception(BACnetSpecialEvent {
1179 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1180 BACnetWeekNDay {
1181 month: BACnetWeekNDay::ANY,
1182 week_of_month: BACnetWeekNDay::ANY,
1183 day_of_week: BACnetWeekNDay::ANY,
1184 },
1185 )),
1186 list_of_time_values: vec![make_tv(0, 0, vec![0xAA])],
1187 event_priority: 15,
1188 });
1189 sched.add_exception(BACnetSpecialEvent {
1190 period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1191 BACnetWeekNDay {
1192 month: BACnetWeekNDay::ANY,
1193 week_of_month: BACnetWeekNDay::ANY,
1194 day_of_week: BACnetWeekNDay::ANY,
1195 },
1196 )),
1197 list_of_time_values: vec![make_tv(0, 0, vec![0xBB])],
1198 event_priority: 5,
1199 });
1200
1201 assert_eq!(
1203 sched.evaluate(0, 12, 0),
1204 PropertyValue::OctetString(vec![0xBB])
1205 );
1206 }
1207
1208 #[test]
1211 fn tick_schedule_returns_none_when_no_refs() {
1212 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1213 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1214 assert!(sched.tick_schedule(0, 12, 0).is_none());
1216 }
1217
1218 #[test]
1219 fn tick_schedule_returns_none_when_value_unchanged() {
1220 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1221 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1222 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1223 oid,
1224 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1225 ));
1226 assert!(sched.tick_schedule(0, 12, 0).is_none());
1228 }
1229
1230 #[test]
1231 fn tick_schedule_returns_value_and_refs_on_change() {
1232 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1233 let target_oid = ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 5).unwrap();
1234 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1235 target_oid,
1236 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1237 ));
1238 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1239
1240 let result = sched.tick_schedule(0, 12, 0);
1241 assert!(result.is_some());
1242 let (value, refs) = result.unwrap();
1243 assert_eq!(value, PropertyValue::OctetString(vec![0x01]));
1244 assert_eq!(refs.len(), 1);
1245 assert_eq!(refs[0].0, target_oid);
1246 assert_eq!(refs[0].1, PropertyIdentifier::PRESENT_VALUE.to_raw());
1247 }
1248
1249 #[test]
1250 fn tick_schedule_updates_present_value() {
1251 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1252 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1253 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1254 oid,
1255 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1256 ));
1257 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1258
1259 let _ = sched.tick_schedule(0, 12, 0);
1260 assert_eq!(
1261 *sched.present_value(),
1262 PropertyValue::OctetString(vec![0x01])
1263 );
1264
1265 assert!(sched.tick_schedule(0, 12, 0).is_none());
1267 }
1268
1269 #[test]
1270 fn tick_schedule_returns_none_when_out_of_service() {
1271 let mut sched = ScheduleObject::new(1, "SCHED-1", PropertyValue::Real(72.0)).unwrap();
1272 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1273 sched.add_object_property_reference(BACnetObjectPropertyReference::new(
1274 oid,
1275 PropertyIdentifier::PRESENT_VALUE.to_raw(),
1276 ));
1277 sched.set_weekly_schedule(0, vec![make_tv(8, 0, vec![0x01])]);
1278 sched.out_of_service = true;
1279
1280 assert!(sched.tick_schedule(0, 12, 0).is_none());
1281 }
1282}