Skip to main content

bacnet_objects/event_enrollment/
alert.rs

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