Skip to main content

bacnet_objects/event_enrollment/
alert.rs

1use std::borrow::Cow;
2
3use bacnet_types::bitstring::EventTransitionBits;
4use bacnet_types::enums::{EventState, NotifyType, ObjectType, PropertyIdentifier};
5use bacnet_types::error::Error;
6use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
7
8use crate::common;
9use crate::event::history::EventHistory;
10use crate::property_metadata::PropertyMetadata;
11use crate::traits::BACnetObject;
12
13use super::metadata;
14
15/// BACnet AlertEnrollment object (type 52).
16///
17/// Provides the Alert Enrollment property surface from ASHRAE 135-2020 Table
18/// 12-61. `Present_Value` names whichever object most recently handed this
19/// enrollment an alert to distribute; recording that source does not itself
20/// evaluate or generate an event. Event_Message_Texts,
21/// Event_Message_Texts_Config and the Event_Algorithm_Inhibit pair come from
22/// the shared reporting options, as on Event Enrollment (#1865).
23pub struct AlertEnrollmentObject {
24    oid: ObjectIdentifier,
25    name: String,
26    description: String,
27    /// Event_State; NORMAL while event detection is disabled.
28    pub(super) event_state: EventState,
29    /// Source of the most recent alert handed to this enrollment.
30    pub present_value: ObjectIdentifier,
31    /// Whether event detection is enabled.
32    ///
33    /// Prefer [`Self::set_event_detection_enable`] so disabling also clears
34    /// stored event state. This field remains public for compatibility;
35    /// property reads and internal transition hooks still enforce the
36    /// disabled-state invariant after a direct assignment. Re-enable through
37    /// the setter as well: a direct FALSE-to-TRUE assignment cannot run the
38    /// reset and may expose state stored before the direct disable.
39    pub event_detection_enable: bool,
40    /// Acked_Transitions: a set flag means that transition was acknowledged.
41    pub(super) acked_transitions: EventTransitionBits,
42    pub(super) event_history: EventHistory,
43    /// Event_Enable: the transitions whose notifications are distributed.
44    pub event_enable: EventTransitionBits,
45    /// Notification class number.
46    pub notification_class: u32,
47    /// Notification category for generated notifications. The local default is
48    /// ALARM; ACK_NOTIFICATION is output-only acknowledgement-flow vocabulary.
49    notify_type: NotifyType,
50}
51
52impl AlertEnrollmentObject {
53    /// Create a new AlertEnrollment object whose Present_Value starts as
54    /// `initial_source`, the latest alert source.
55    pub fn new(
56        instance: u32,
57        name: impl Into<String>,
58        initial_source: ObjectIdentifier,
59    ) -> Result<Self, Error> {
60        let oid = ObjectIdentifier::new(ObjectType::ALERT_ENROLLMENT, instance)?;
61        Ok(Self {
62            oid,
63            name: name.into(),
64            description: String::new(),
65            event_state: EventState::NORMAL,
66            present_value: initial_source,
67            event_detection_enable: true,
68            acked_transitions: EventTransitionBits::all(),
69            event_history: EventHistory::default(),
70            event_enable: EventTransitionBits::all(),
71            notification_class: 0,
72            notify_type: NotifyType::ALARM,
73        })
74    }
75
76    /// Record `source` as the object behind the latest alert.
77    ///
78    /// This source-ownership hook updates only `Present_Value`; it does not
79    /// evaluate an alert, change event/acknowledgement history, or generate a
80    /// notification.
81    pub fn record_alert_source(&mut self, source: ObjectIdentifier) {
82        self.present_value = source;
83    }
84
85    /// Enable or disable event detection.
86    ///
87    /// Disabling applies the Clause 13.2.2.1 initial conditions immediately.
88    pub fn set_event_detection_enable(&mut self, enabled: bool) {
89        if !enabled || !self.event_detection_enable {
90            self.event_state = EventState::NORMAL;
91            self.acked_transitions = EventTransitionBits::all();
92            self.event_history.reset();
93        }
94        self.event_detection_enable = enabled;
95    }
96
97    /// Alert-local projection of the universal properties that Table 12-61
98    /// serves. In particular, the shared common-property macro is intentionally
99    /// not used because it would also expose Status_Flags, Out_Of_Service, and
100    /// Reliability, none of which is a Table 12-61 row.
101    fn read_alert_common_property(
102        &self,
103        property: PropertyIdentifier,
104        array_index: Option<u32>,
105    ) -> Option<Result<PropertyValue, Error>> {
106        match property {
107            PropertyIdentifier::OBJECT_IDENTIFIER => {
108                Some(Ok(PropertyValue::ObjectIdentifier(self.oid)))
109            }
110            PropertyIdentifier::OBJECT_NAME => {
111                Some(Ok(PropertyValue::CharacterString(self.name.clone())))
112            }
113            PropertyIdentifier::DESCRIPTION => {
114                Some(Ok(PropertyValue::CharacterString(self.description.clone())))
115            }
116            PropertyIdentifier::PROPERTY_LIST => {
117                let properties = self.property_list();
118                Some(common::read_property_list_property(
119                    &properties,
120                    array_index,
121                ))
122            }
123            _ => None,
124        }
125    }
126}
127
128impl BACnetObject for AlertEnrollmentObject {
129    fn object_identifier(&self) -> ObjectIdentifier {
130        self.oid
131    }
132
133    fn object_name(&self) -> &str {
134        &self.name
135    }
136
137    fn read_property(
138        &self,
139        property: PropertyIdentifier,
140        array_index: Option<u32>,
141    ) -> Result<PropertyValue, Error> {
142        // The history arrays read their initial condition while detection is
143        // off, even after a direct field assignment skipped the reset.
144        if !self.event_detection_enable
145            && matches!(
146                property,
147                PropertyIdentifier::EVENT_TIME_STAMPS | PropertyIdentifier::EVENT_MESSAGE_TEXTS
148            )
149        {
150            return EventHistory::default()
151                .read(property, array_index)
152                .expect("EventHistory handles the history arrays");
153        }
154        // Event_Time_Stamps, Event_Message_Texts and the reporting options
155        // (Event_Message_Texts_Config and the Event_Algorithm_Inhibit pair),
156        // which Table 12-61 lists as optional rows (#1865).
157        if let Some(result) = self.event_history.read(property, array_index) {
158            return result;
159        }
160        if let Some(result) = self.read_alert_common_property(property, array_index) {
161            return result;
162        }
163        match property {
164            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
165                ObjectType::ALERT_ENROLLMENT.to_raw(),
166            )),
167            p if p == PropertyIdentifier::PRESENT_VALUE => {
168                Ok(PropertyValue::ObjectIdentifier(self.present_value))
169            }
170            p if p == PropertyIdentifier::EVENT_DETECTION_ENABLE => {
171                Ok(PropertyValue::Boolean(self.event_detection_enable))
172            }
173            p if p == PropertyIdentifier::EVENT_ENABLE => Ok(PropertyValue::BitString {
174                unused_bits: 5,
175                data: vec![self.event_enable.to_bacnet()],
176            }),
177            p if p == PropertyIdentifier::NOTIFICATION_CLASS => {
178                Ok(PropertyValue::Unsigned(self.notification_class as u64))
179            }
180            p if p == PropertyIdentifier::NOTIFY_TYPE => {
181                Ok(PropertyValue::Enumerated(self.notify_type.to_raw()))
182            }
183            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(
184                if self.event_detection_enable {
185                    self.event_state
186                } else {
187                    EventState::NORMAL
188                }
189                .to_raw(),
190            )),
191            p if p == PropertyIdentifier::ACKED_TRANSITIONS => Ok(PropertyValue::BitString {
192                unused_bits: 5,
193                data: vec![if self.event_detection_enable {
194                    self.acked_transitions
195                } else {
196                    EventTransitionBits::all()
197                }
198                .to_bacnet()],
199            }),
200            _ => Err(common::unknown_property_error()),
201        }
202    }
203
204    fn write_property(
205        &mut self,
206        property: PropertyIdentifier,
207        array_index: Option<u32>,
208        value: PropertyValue,
209        _priority: Option<u8>,
210    ) -> Result<(), Error> {
211        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
212            if let PropertyValue::Boolean(v) = value {
213                self.set_event_detection_enable(v);
214                return Ok(());
215            }
216            return Err(common::invalid_data_type_error());
217        }
218        if property == PropertyIdentifier::EVENT_ENABLE {
219            // BACnetEventTransitionBits is a 3-bit production (Clause 21):
220            // the written BitString must declare its canonical shape.
221            if let PropertyValue::BitString { unused_bits, data } = &value {
222                let byte = common::check_fixed_width_bit_string(*unused_bits, data, 3)?;
223                self.event_enable = EventTransitionBits::from_bacnet(&[byte]);
224                return Ok(());
225            }
226            return Err(common::invalid_data_type_error());
227        }
228        if property == PropertyIdentifier::NOTIFICATION_CLASS {
229            if let PropertyValue::Unsigned(v) = value {
230                self.notification_class = common::u64_to_u32(v)?;
231                return Ok(());
232            }
233            return Err(common::invalid_data_type_error());
234        }
235        if property == PropertyIdentifier::NOTIFY_TYPE {
236            if let PropertyValue::Enumerated(v) = value {
237                let notify_type = NotifyType::from_raw(v);
238                if notify_type != NotifyType::ALARM && notify_type != NotifyType::EVENT {
239                    return Err(common::value_out_of_range_error());
240                }
241                self.notify_type = notify_type;
242                return Ok(());
243            }
244            return Err(common::invalid_data_type_error());
245        }
246        if let Some(result) =
247            self.event_history
248                .write(property, array_index, &value, self.event_detection_enable)
249        {
250            return result;
251        }
252        if let Some(result) = common::write_description(&mut self.description, property, &value) {
253            return result;
254        }
255        Err(crate::common::unhandled_write_error(
256            self.property_metadata().as_ref(),
257            property,
258            array_index,
259        ))
260    }
261
262    fn set_event_state_internal(&mut self, state: EventState) -> Result<(), Error> {
263        if !self.event_detection_enable && state != EventState::NORMAL {
264            return Err(common::write_access_denied_error());
265        }
266        self.event_state = state;
267        Ok(())
268    }
269
270    fn set_acked_transitions_internal(
271        &mut self,
272        transition_bit: EventTransitionBits,
273        acknowledged: bool,
274    ) -> Result<(), Error> {
275        if !self.event_detection_enable {
276            return Err(common::write_access_denied_error());
277        }
278        let transition_bit = transition_bit & EventTransitionBits::all();
279        if acknowledged {
280            self.acked_transitions.insert(transition_bit);
281        } else {
282            // Alert Enrollment never requires acknowledgment for TO_NORMAL
283            // (Clause 12.52.8), so that bit cannot enter the unacknowledged
284            // state even if a generic transition hook asks to clear it.
285            self.acked_transitions
286                .remove(transition_bit.difference(EventTransitionBits::TO_NORMAL));
287        }
288        self.acked_transitions
289            .insert(EventTransitionBits::TO_NORMAL);
290        Ok(())
291    }
292
293    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
294        matches!(
295            property,
296            PropertyIdentifier::DESCRIPTION
297                | PropertyIdentifier::EVENT_DETECTION_ENABLE
298                | PropertyIdentifier::EVENT_ENABLE
299                | PropertyIdentifier::NOTIFICATION_CLASS
300                | PropertyIdentifier::NOTIFY_TYPE
301                | PropertyIdentifier::EVENT_MESSAGE_TEXTS_CONFIG
302                | PropertyIdentifier::EVENT_ALGORITHM_INHIBIT_REF
303                | PropertyIdentifier::EVENT_ALGORITHM_INHIBIT
304        )
305    }
306
307    fn event_algorithm_inhibit_reference_internal(
308        &self,
309    ) -> Option<bacnet_types::constructed::BACnetObjectPropertyReference> {
310        self.event_history.options.inhibit_reference().cloned()
311    }
312
313    fn follow_event_algorithm_inhibit_internal(&mut self, inhibit: bool) -> bool {
314        self.event_history.options.follow(inhibit)
315    }
316
317    fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
318        Cow::Borrowed(metadata::ALERT_ENROLLMENT_PROPERTIES)
319    }
320
321    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
322        crate::property_metadata::property_list_from_metadata(metadata::ALERT_ENROLLMENT_PROPERTIES)
323    }
324}