Skip to main content

bacnet_objects/event_enrollment/
alert.rs

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