Skip to main content

bacnet_objects/schedule/
calendar.rs

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