Skip to main content

bacnet_objects/schedule/
calendar.rs

1//! Calendar (type 6), Clause 12.9.
2
3use std::borrow::Cow;
4use std::sync::Arc;
5
6use bacnet_types::calendar::SpecificDate;
7use bacnet_types::constructed::BACnetCalendarEntry;
8use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
11
12use super::{calendar_metadata, date_list};
13use crate::clock::ClockReader;
14use crate::common::{self, read_property_list_property};
15use crate::traits::BACnetObject;
16
17/// BACnet Calendar object.
18///
19/// Present_Value is TRUE when the device's local date matches an entry in
20/// Date_List (Clause 12.9). The object reads that date from the database's
21/// clock each time Present_Value is read, so the value follows the date with
22/// no tick; without a clock, or while the clock has no valid local date, it
23/// is FALSE. [`is_active_on`](Self::is_active_on) answers for any other day.
24///
25/// Date_List is a BACnetLIST of `BACnetCalendarEntry`, each entry under its
26/// Clause 21 CHOICE tag on the wire. It is network-writable (WriteProperty,
27/// AddListElement and RemoveListElement). A write whose entries hold a value
28/// outside its Clause 21 range is refused with VALUE_OUT_OF_RANGE and leaves
29/// the list unchanged.
30///
31/// The object serves only properties its table (Clause 12.9, Table 12-11)
32/// defines. That table has no Status_Flags, Event_State, Out_Of_Service or
33/// Reliability, so reads and writes of those return UNKNOWN_PROPERTY.
34pub struct CalendarObject {
35    oid: ObjectIdentifier,
36    name: String,
37    description: String,
38    date_list: Vec<BACnetCalendarEntry>,
39    /// The database's wall clock, the source of the local date.
40    clock: Option<Arc<dyn ClockReader>>,
41}
42
43impl CalendarObject {
44    /// Create a new Calendar object with an empty date list.
45    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
46        let oid = ObjectIdentifier::new(ObjectType::CALENDAR, instance)?;
47        Ok(Self {
48            oid,
49            name: name.into(),
50            description: String::new(),
51            date_list: Vec::new(),
52            clock: None,
53        })
54    }
55
56    /// Set the description string.
57    pub fn set_description(&mut self, desc: impl Into<String>) {
58        self.description = desc.into();
59    }
60
61    /// Append a calendar entry to the date_list.
62    ///
63    /// Refuses, as a network write would, an entry with a value outside its
64    /// Clause 21 range (PROPERTY / VALUE_OUT_OF_RANGE) and an entry past the
65    /// 1,024-entry cap (RESOURCES / NO_SPACE_TO_WRITE_PROPERTY).
66    pub fn add_date_entry(&mut self, entry: BACnetCalendarEntry) -> Result<(), Error> {
67        if !entry.is_valid() {
68            return Err(common::value_out_of_range_error());
69        }
70        if self.date_list.len() >= date_list::MAX_DATE_LIST_ENTRIES {
71            return Err(date_list::no_space_error());
72        }
73        self.date_list.push(entry);
74        Ok(())
75    }
76
77    /// Remove all entries from the date_list.
78    pub fn clear_date_list(&mut self) {
79        self.date_list.clear();
80    }
81
82    /// The current date_list entries, as configured or last written.
83    pub fn date_list(&self) -> &[BACnetCalendarEntry] {
84        &self.date_list
85    }
86
87    /// Whether `day` matches a Date_List entry: the Present_Value the
88    /// calendar has on that day.
89    pub fn is_active_on(&self, day: SpecificDate) -> bool {
90        self.date_list.iter().any(|entry| entry.matches(day))
91    }
92
93    /// Present_Value now: whether the clock's local date matches an entry.
94    /// FALSE without a clock or a valid local date.
95    pub fn present_value(&self) -> bool {
96        self.clock
97            .as_ref()
98            .and_then(|clock| clock.read_clock())
99            .and_then(|frame| SpecificDate::from_date(&frame.local_date))
100            .is_some_and(|today| self.is_active_on(today))
101    }
102}
103
104impl BACnetObject for CalendarObject {
105    fn object_identifier(&self) -> ObjectIdentifier {
106        self.oid
107    }
108
109    fn object_name(&self) -> &str {
110        &self.name
111    }
112
113    fn read_property(
114        &self,
115        property: PropertyIdentifier,
116        array_index: Option<u32>,
117    ) -> Result<PropertyValue, Error> {
118        match property {
119            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
120                Ok(PropertyValue::ObjectIdentifier(self.oid))
121            }
122            p if p == PropertyIdentifier::OBJECT_NAME => {
123                Ok(PropertyValue::CharacterString(self.name.clone()))
124            }
125            p if p == PropertyIdentifier::DESCRIPTION => {
126                Ok(PropertyValue::CharacterString(self.description.clone()))
127            }
128            p if p == PropertyIdentifier::OBJECT_TYPE => {
129                Ok(PropertyValue::Enumerated(ObjectType::CALENDAR.to_raw()))
130            }
131            p if p == PropertyIdentifier::PRESENT_VALUE => {
132                Ok(PropertyValue::Boolean(self.present_value()))
133            }
134            p if p == PropertyIdentifier::DATE_LIST => Ok(date_list::read(&self.date_list)),
135            p if p == PropertyIdentifier::PROPERTY_LIST => {
136                read_property_list_property(&self.property_list(), array_index)
137            }
138            _ => Err(Error::Protocol {
139                class: ErrorClass::PROPERTY.to_raw() as u32,
140                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
141            }),
142        }
143    }
144
145    fn write_property(
146        &mut self,
147        property: PropertyIdentifier,
148        array_index: Option<u32>,
149        value: PropertyValue,
150        _priority: Option<u8>,
151    ) -> Result<(), Error> {
152        if let Some(result) = common::write_description(&mut self.description, property, &value) {
153            return result;
154        }
155        if property == PropertyIdentifier::DATE_LIST {
156            // A BACnetLIST takes no index; the services gate this first.
157            if array_index.is_some() {
158                return Err(common::property_is_not_an_array_error());
159            }
160            self.date_list = date_list::decode_write(value)?;
161            return Ok(());
162        }
163        if property == PropertyIdentifier::PRESENT_VALUE {
164            return Err(common::write_access_denied_error());
165        }
166        Err(common::unhandled_write_error(
167            self.property_metadata().as_ref(),
168            property,
169            array_index,
170        ))
171    }
172
173    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
174        calendar_metadata::for_object(self)
175    }
176
177    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
178        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
179    }
180
181    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
182        self.clock = clock;
183    }
184
185    fn calendar_state_internal(&self, day: SpecificDate) -> Option<bool> {
186        Some(self.is_active_on(day))
187    }
188}