Skip to main content

bacnet_objects/event_enrollment/
mod.rs

1//! EventEnrollment (type 9) object per ASHRAE 135-2020 Clause 12.12.
2
3use crate::prelude::*;
4use alloc::borrow::Cow;
5use bacnet_types::bitstring::EventTransitionBits;
6use bacnet_types::constructed::{
7    BACnetDeviceObjectPropertyReference, BACnetEventParameter, FaultParameters,
8};
9use bacnet_types::enums::{
10    ErrorClass, ErrorCode, EventState, EventType, NotifyType, ObjectType, PropertyIdentifier,
11    Reliability,
12};
13use bacnet_types::error::Error;
14use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue, StatusFlags};
15
16use crate::common::{self, read_common_properties};
17use crate::event::history::EventHistory;
18use crate::event::{
19    EnrollmentSummaryCapability, EventTransitionCommit, EventTransitionCommitError,
20};
21use crate::property_metadata::PropertyMetadata;
22use crate::traits::BACnetObject;
23
24mod alert;
25mod metadata;
26mod parameters;
27mod state;
28mod transition;
29pub use alert::AlertEnrollmentObject;
30pub use state::{EventEnrollmentEvalState, EventEnrollmentMonitoredSource, EventEnrollmentPending};
31pub use transition::EventEnrollmentReliabilityCommit;
32
33/// BACnet EventEnrollment object.
34///
35/// Provides algorithmic event detection for a referenced object property.
36/// The `event_parameters` are stored as a structured
37/// [`BACnetEventParameter`], preserving algorithm alternatives and unknown
38/// (vendor/reserved) values across a complete property round trip.
39pub struct EventEnrollmentObject {
40    oid: ObjectIdentifier,
41    name: String,
42    description: String,
43    /// The algorithm the legacy octet layout of Event_Parameters runs, which
44    /// names none itself; every other alternative names its own (#1868).
45    legacy_event_type: EventType,
46    notify_type: NotifyType,
47    event_parameters: BACnetEventParameter,
48    object_property_reference: Option<BACnetDeviceObjectPropertyReference>,
49    event_state: EventState,
50    event_enable: EventTransitionBits,
51    acked_transitions: EventTransitionBits,
52    event_history: EventHistory,
53    event_detection_enable: bool,
54    notification_class: u32,
55    fault_parameters: Option<FaultParameters>,
56    status_flags: StatusFlags,
57    reliability: Reliability,
58    /// `Time_Delay_Normal` (property 356, Table 12-14 conformance O): Clause
59    /// 12.12 feeds this value to the enrollment's algorithm as its
60    /// pTimeDelayNormal input. `None` is the not-configured case and takes on
61    /// the `Time_Delay` carried inside `event_parameters` (Table 12-15 maps
62    /// `Time_Delay` to pTimeDelay for every evaluated algorithm), following
63    /// Clause 13.3's fallback from absent pTimeDelayNormal to pTimeDelay.
64    time_delay_normal: Option<u32>,
65    /// Delayed transition counting down, if any. In-memory only.
66    pending: Option<EventEnrollmentPending>,
67    /// Effective source of the private evaluation state. In-memory only.
68    monitored_reference: Option<EventEnrollmentMonitoredSource>,
69    /// CHANGE_OF_VALUE detection baseline (Clause 13.3.3). In-memory only.
70    cov_baseline: Option<PropertyValue>,
71    /// Domain-tagged monitored value that caused the last OFFNORMAL transition
72    /// (Clause 13.3.2 condition (c)). In-memory only.
73    last_offnormal_value: Option<u64>,
74}
75
76impl EventEnrollmentObject {
77    /// Create a new EventEnrollment object.
78    ///
79    /// Event_Type follows Event_Parameters (Clause 12.12.5): it reads NONE
80    /// while the parameters are the unset `none` alternative and the
81    /// matching type once parameters are set. `legacy_event_type` only
82    /// matters for the pre-framing octet layout of Event_Parameters, which
83    /// carries no algorithm of its own: it is the algorithm that layout runs
84    /// and the Event_Type served while it is held.
85    pub fn new(
86        instance: u32,
87        name: impl Into<String>,
88        legacy_event_type: EventType,
89    ) -> Result<Self, Error> {
90        let oid = ObjectIdentifier::new(ObjectType::EVENT_ENROLLMENT, instance)?;
91        Ok(Self {
92            oid,
93            name: name.into(),
94            description: String::new(),
95            legacy_event_type,
96            notify_type: NotifyType::ALARM,
97            event_parameters: Self::UNSET_EVENT_PARAMETERS,
98            object_property_reference: None,
99            event_state: EventState::NORMAL,
100            event_enable: EventTransitionBits::all(),
101            // Clause 12.12 starts each flag TRUE until a transition of its
102            // kind first happens on the object. That all-TRUE initial value
103            // is also the initial condition the detection-disabled reset
104            // restores, so `RESET_ACKED_TRANSITIONS` names it once.
105            acked_transitions: Self::RESET_ACKED_TRANSITIONS,
106            event_history: EventHistory::default(),
107            event_detection_enable: true,
108            notification_class: 0,
109            fault_parameters: None,
110            status_flags: StatusFlags::empty(),
111            reliability: Reliability::NO_FAULT_DETECTED,
112            // Absent so the delay behavior equals the normative pTimeDelay
113            // fallback until a client writes the property — never an error,
114            // never a zero.
115            time_delay_normal: None,
116            pending: None,
117            monitored_reference: None,
118            cov_baseline: None,
119            last_offnormal_value: None,
120        })
121    }
122
123    /// `Acked_Transitions` in its initial condition: every transition flag TRUE,
124    /// the value a flag holds until its transition kind first occurs on the
125    /// object (ASHRAE 135-2020 Clause 12.12).
126    const RESET_ACKED_TRANSITIONS: EventTransitionBits = EventTransitionBits::all();
127
128    /// `Event_Parameters` before anything sets them: the `none` alternative
129    /// of the BACnetEventParameter CHOICE (context tag 20, a NULL; Clause
130    /// 21), so an unset enrollment serves a member of the property's
131    /// datatype (#1868). The evaluator has no algorithm to run for it.
132    const UNSET_EVENT_PARAMETERS: BACnetEventParameter = BACnetEventParameter::None;
133
134    /// Put the object in the state ASHRAE 135-2020 Clause 13.2.2.1 prescribes
135    /// for disabled detection (`Event_Detection_Enable` FALSE): Event_State
136    /// reads NORMAL, the three per-transition properties (Event_Time_Stamps,
137    /// Event_Message_Texts, Acked_Transitions) go back to their initial
138    /// values, and no transition is generated while detection stays off.
139    ///
140    /// The monitored-source identity, pending countdown, and both baselines
141    /// are cleared too: they are extensions of the same event-state-detection
142    /// state machine whose evaluation the clause suspends, so a stale
143    /// countdown must not survive into the next
144    /// enabled period and fire against a condition the object no longer
145    /// observes. The intrinsic types make the same choice for their detectors
146    /// (`analog/input.rs` clears `detector.pending` on the identical write).
147    /// The COV baseline's initialization on re-enable is the local matter
148    /// Clause 13.3.3 assigns it; clearing is consistent with the first-sample
149    /// policy.
150    fn apply_detection_disabled_reset(&mut self) {
151        self.event_state = EventState::NORMAL;
152        self.acked_transitions = Self::RESET_ACKED_TRANSITIONS;
153        self.event_history.reset();
154        self.pending = None;
155        self.monitored_reference = None;
156        self.cov_baseline = None;
157        self.last_offnormal_value = None;
158    }
159
160    /// Set the description string.
161    pub fn set_description(&mut self, desc: impl Into<String>) {
162        self.description = desc.into();
163    }
164
165    /// Set Object_Property_Reference, the property the enrollment monitors,
166    /// or `None` for none. The property is read-only over the network.
167    /// A reference whose object or Device is at the reserved instance
168    /// 4194303 is the unset form, so it leaves the enrollment without one
169    /// (#1417).
170    ///
171    /// A reference whose device identifier isn't a Device object is refused
172    /// with PROPERTY / VALUE_OUT_OF_RANGE and the reference set before is
173    /// kept (#1308).
174    pub fn set_object_property_reference(
175        &mut self,
176        reference: Option<BACnetDeviceObjectPropertyReference>,
177    ) -> Result<(), Error> {
178        if let Some(reference) = &reference {
179            crate::device_reference::check_device_member(reference.device_identifier)?;
180        }
181        self.object_property_reference = reference.and_then(crate::device_reference::set_or_unset);
182        self.pending = None;
183        Ok(())
184    }
185
186    /// Set the structured event parameters.
187    pub fn set_event_parameters(&mut self, params: BACnetEventParameter) {
188        self.event_parameters = params;
189        self.pending = None;
190    }
191
192    /// Set the fault parameters for this event enrollment.
193    pub fn set_fault_parameters(&mut self, fp: Option<FaultParameters>) {
194        self.fault_parameters = fp;
195    }
196
197    /// Set the event state.
198    ///
199    /// A configuration/seeding helper, not a lifecycle path — the evaluator
200    /// uses [`BACnetObject::set_event_state_internal`]. It honors the same
201    /// Clause 13.2.2.1 rule: while `Event_Detection_Enable` is FALSE the object
202    /// must read NORMAL, so a non-NORMAL seed is ignored rather than silently
203    /// breaking the invariant. Without this the public API would offer a way
204    /// around a guard the rest of the object enforces.
205    pub fn set_event_state(&mut self, state: EventState) {
206        if !self.event_detection_enable && state != EventState::NORMAL {
207            return;
208        }
209        self.event_state = state;
210    }
211
212    /// Set the notification class.
213    pub fn set_notification_class(&mut self, nc: u32) {
214        self.notification_class = nc;
215    }
216
217    /// Set `Event_Enable`: the transitions whose notifications are distributed.
218    ///
219    /// Flags outside the three named transitions are dropped.
220    pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
221        self.event_enable = enable & EventTransitionBits::all();
222    }
223
224    /// Set `Time_Delay_Normal` (the pTimeDelayNormal parameter). `None`
225    /// restores the not-configured case, which takes on the
226    /// `Event_Parameters` `Time_Delay` value (Clause 13.3 fallback).
227    pub fn set_time_delay_normal(&mut self, delay: Option<u32>) {
228        self.time_delay_normal = delay;
229        self.pending = None;
230    }
231
232    /// The pTimeDelay the stored `Event_Parameters` supply: the `time_delay`
233    /// member of every algorithm that has one (Table 12-15), so an unset
234    /// Time_Delay_Normal reads back the algorithm's delay (#2090), whether
235    /// or not the server runs it. The alternatives with no time-delay slot
236    /// (`none`, buffer-ready, access-event, extended, an unassigned tag and
237    /// the `0xFF` legacy octet layout) contribute zero, so their evaluation
238    /// fires immediately, exactly as it did before delay honoring existed.
239    fn event_parameters_time_delay(&self) -> u32 {
240        use BACnetEventParameter as P;
241        match &self.event_parameters {
242            P::ChangeOfBitstring { time_delay, .. }
243            | P::ChangeOfState { time_delay, .. }
244            | P::ChangeOfValue { time_delay, .. }
245            | P::FloatingLimit { time_delay, .. }
246            | P::OutOfRange { time_delay, .. }
247            | P::CommandFailure { time_delay, .. }
248            | P::ChangeOfLifeSafety { time_delay, .. }
249            | P::UnsignedRange { time_delay, .. }
250            | P::DoubleOutOfRange { time_delay, .. }
251            | P::SignedOutOfRange { time_delay, .. }
252            | P::UnsignedOutOfRange { time_delay, .. }
253            | P::ChangeOfCharacterstring { time_delay, .. }
254            | P::ChangeOfStatusFlags { time_delay, .. }
255            | P::ChangeOfDiscreteValue { time_delay }
256            | P::ChangeOfTimer { time_delay, .. } => *time_delay,
257            _ => 0,
258        }
259    }
260
261    /// Event_Type as Event_Parameters gives it (Clause 12.12.5): each
262    /// framed alternative's tag is its event type (`none`, tag 20, is NONE).
263    /// The legacy octet layout uses the constructor's type, and so does an
264    /// opaque alternative at tag 64 or above: such a tag names no event type
265    /// the standard reserves (0 to 63), so nothing is derived from it.
266    fn event_type(&self) -> EventType {
267        match &self.event_parameters {
268            BACnetEventParameter::Opaque { tag, .. } if *tag >= 64 => self.legacy_event_type,
269            parameters => EventType::from_raw(u32::from(parameters.tag())),
270        }
271    }
272
273    fn effective_fault_parameters(&self) -> &FaultParameters {
274        self.fault_parameters
275            .as_ref()
276            .unwrap_or(&FaultParameters::FaultNone)
277    }
278}
279
280impl BACnetObject for EventEnrollmentObject {
281    fn object_identifier(&self) -> ObjectIdentifier {
282        self.oid
283    }
284
285    fn object_name(&self) -> &str {
286        &self.name
287    }
288
289    fn enrollment_summary_capability_internal(&self) -> Option<EnrollmentSummaryCapability> {
290        // Any enrollment with a monitored reference can fault: the monitored
291        // object's fault and the configuration checks run before the
292        // parameters are looked at, so whatever Event_Parameters hold (`none`,
293        // an unmodeled alternative, Extended) its TO_FAULT is listed and
294        // acknowledged (#1868). Event_Detection_Enable FALSE is left to the
295        // summary and acknowledgment paths.
296        self.object_property_reference
297            .is_some()
298            .then(|| EnrollmentSummaryCapability {
299                event_type: self.event_type(),
300                last_transition: self.event_history.last_transition(),
301            })
302    }
303
304    fn read_property(
305        &self,
306        property: PropertyIdentifier,
307        array_index: Option<u32>,
308    ) -> Result<PropertyValue, Error> {
309        if property == PropertyIdentifier::STATUS_FLAGS {
310            // Clause 12.12 fixes OVERRIDDEN and OUT_OF_SERVICE at FALSE;
311            // IN_ALARM follows Event_State and FAULT follows Reliability.
312            return Ok(common::compute_status_flags(
313                StatusFlags::empty(),
314                self.reliability,
315                false,
316                self.event_state,
317            ));
318        }
319        // Table 12-14 has no Out_Of_Service (#1064).
320        if let Some(result) =
321            read_common_properties!(self, property, array_index, no_out_of_service)
322        {
323            return result;
324        }
325        // Event_Time_Stamps, Event_Message_Texts and the reporting options
326        // (Event_Message_Texts_Config and the Event_Algorithm_Inhibit pair,
327        // #1507), all optional or required rows of Table 12-14.
328        if let Some(result) = self.event_history.read(property, array_index) {
329            return result;
330        }
331        match property {
332            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
333                ObjectType::EVENT_ENROLLMENT.to_raw(),
334            )),
335            p if p == PropertyIdentifier::EVENT_TYPE => {
336                Ok(PropertyValue::Enumerated(self.event_type().to_raw()))
337            }
338            p if p == PropertyIdentifier::NOTIFY_TYPE => {
339                Ok(PropertyValue::Enumerated(self.notify_type.to_raw()))
340            }
341            p if p == PropertyIdentifier::EVENT_PARAMETERS => {
342                let mut buf = bytes::BytesMut::new();
343                bacnet_encoding::constructed::encode_event_parameter(
344                    &mut buf,
345                    &self.event_parameters,
346                )?;
347                Ok(PropertyValue::ApplicationData(buf.to_vec()))
348            }
349            // Table 12-14's BACnetDeviceObjectPropertyReference, its optional
350            // index and Device members present only when set (#1182); the
351            // unset form while the enrollment has no reference (#1417).
352            p if p == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE => {
353                Ok(crate::device_reference::optional_reference_value(
354                    self.object_property_reference.as_ref(),
355                    ObjectType::ANALOG_INPUT,
356                ))
357            }
358            p if p == PropertyIdentifier::EVENT_STATE => {
359                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
360            }
361            p if p == PropertyIdentifier::EVENT_ENABLE => Ok(PropertyValue::BitString {
362                unused_bits: 5,
363                data: vec![self.event_enable.to_bacnet()],
364            }),
365            p if p == PropertyIdentifier::ACKED_TRANSITIONS => Ok(PropertyValue::BitString {
366                unused_bits: 5,
367                data: vec![self.acked_transitions.to_bacnet()],
368            }),
369            p if p == PropertyIdentifier::EVENT_DETECTION_ENABLE => {
370                Ok(PropertyValue::Boolean(self.event_detection_enable))
371            }
372            p if p == PropertyIdentifier::NOTIFICATION_CLASS => {
373                Ok(PropertyValue::Unsigned(self.notification_class as u64))
374            }
375            p if p == PropertyIdentifier::FAULT_TYPE => Ok(PropertyValue::Enumerated(
376                self.effective_fault_parameters().fault_type().to_raw(),
377            )),
378            p if p == PropertyIdentifier::FAULT_PARAMETERS => {
379                let mut buf = bytes::BytesMut::new();
380                bacnet_encoding::constructed::encode_fault_parameters(
381                    &mut buf,
382                    self.effective_fault_parameters(),
383                )?;
384                Ok(PropertyValue::ApplicationData(buf.to_vec()))
385            }
386            p if p == PropertyIdentifier::TIME_DELAY_NORMAL => {
387                // Clause 13.3 supplies pTimeDelay as the fallback when
388                // pTimeDelayNormal is absent —
389                // the read-back of an unwritten Time_Delay_Normal is the
390                // Event_Parameters Time_Delay, matching the algorithm's
391                // behavior (mirrors the intrinsic types' read arm).
392                Ok(PropertyValue::Unsigned(
393                    self.time_delay_normal
394                        .unwrap_or_else(|| self.event_parameters_time_delay())
395                        as u64,
396                ))
397            }
398            _ => Err(common::unknown_property_error()),
399        }
400    }
401
402    fn write_property(
403        &mut self,
404        property: PropertyIdentifier,
405        array_index: Option<u32>,
406        value: PropertyValue,
407        _priority: Option<u8>,
408    ) -> Result<(), Error> {
409        if property == PropertyIdentifier::NOTIFY_TYPE {
410            // BACnetNotifyType is a closed three-value production {alarm,
411            // event, ack-notification} (Clause 21); out-of-production values
412            // are PROPERTY / VALUE_OUT_OF_RANGE (Clause 15.9.1.3).
413            if let PropertyValue::Enumerated(v) = value {
414                let notify_type = NotifyType::from_raw(v);
415                if !NotifyType::ALL_NAMED.iter().any(|&(_, n)| n == notify_type) {
416                    return Err(common::value_out_of_range_error());
417                }
418                self.notify_type = notify_type;
419                return Ok(());
420            }
421            return Err(common::invalid_data_type_error());
422        }
423        if property == PropertyIdentifier::NOTIFICATION_CLASS {
424            if let PropertyValue::Unsigned(v) = value {
425                self.notification_class = common::u64_to_u32(v)?;
426                return Ok(());
427            }
428            return Err(common::invalid_data_type_error());
429        }
430        if property == PropertyIdentifier::EVENT_ENABLE {
431            // BACnetEventTransitionBits is a 3-bit production (Clause 21):
432            // the written BitString must declare its canonical shape.
433            if let PropertyValue::BitString { unused_bits, data } = &value {
434                let byte = common::check_fixed_width_bit_string(*unused_bits, data, 3)?;
435                self.event_enable = EventTransitionBits::from_bacnet(&[byte]);
436                return Ok(());
437            }
438            return Err(common::invalid_data_type_error());
439        }
440        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
441            if let PropertyValue::Boolean(v) = value {
442                self.event_detection_enable = v;
443                // Clause 12.12 states the disabled condition as an invariant —
444                // disabled detection requires a persistent NORMAL Event_State —
445                // not as an action taken later. Resetting here rather than
446                // leaving it to the periodic evaluator closes the window in
447                // which a disabled object would still answer ReadProperty and
448                // the event-summarization services with a stale alarm state.
449                if !v {
450                    self.apply_detection_disabled_reset();
451                }
452                return Ok(());
453            }
454            return Err(common::invalid_data_type_error());
455        }
456        // EVENT_STATE is algorithmically derived (ASHRAE 135-2020 Clause 12.12)
457        // and read-only over the network: a WriteProperty of EVENT_STATE falls
458        // through to WRITE_ACCESS_DENIED below. The evaluator sets it through
459        // the internal `set_event_state_internal` trait method instead, so the
460        // network route and the internal lifecycle path no longer share an
461        // access path (issue #130).
462        if property == PropertyIdentifier::EVENT_PARAMETERS {
463            self.set_event_parameters(parameters::decode_event_parameters(value)?);
464            return Ok(());
465        }
466        if property == PropertyIdentifier::FAULT_PARAMETERS {
467            // The context-tagged `none` choice is the unset form (#1417).
468            let parameters = parameters::decode_fault_parameters(value)?;
469            self.fault_parameters =
470                (!matches!(parameters, FaultParameters::FaultNone)).then_some(parameters);
471            return Ok(());
472        }
473        if property == PropertyIdentifier::TIME_DELAY_NORMAL {
474            // Table 12-14 codes the property O, not W; accepting the write is
475            // the Clause 12.1.2 implementor's option the intrinsic types
476            // already exercise, and is what makes the Clause 13.3 delay
477            // asymmetry commissionable on an enrollment at all.
478            if let PropertyValue::Unsigned(v) = value {
479                self.set_time_delay_normal(Some(common::u64_to_u32(v)?));
480                return Ok(());
481            }
482            return Err(common::invalid_data_type_error());
483        }
484        if let Some(result) =
485            self.event_history
486                .write(property, array_index, &value, self.event_detection_enable)
487        {
488            return result;
489        }
490        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
491            return result;
492        }
493        if let Some(result) = common::write_description(&mut self.description, property, &value) {
494            return result;
495        }
496        Err(crate::common::unhandled_write_error(
497            self.property_metadata().as_ref(),
498            property,
499            array_index,
500        ))
501    }
502
503    /// Internal lifecycle path for the algorithmically-derived `Event_State`.
504    ///
505    /// The evaluator calls this — not `write_property(EVENT_STATE, …)` — so the
506    /// network route (which rejects `EVENT_STATE`) and the internal lifecycle
507    /// path are distinct (issue #130). Stores the modeled state verbatim; the
508    /// only caller is the trusted server evaluator.
509    ///
510    /// Refuses any non-NORMAL state while `Event_Detection_Enable` is FALSE.
511    /// Clause 13.2.2.1 prohibits transitions in that case,
512    /// and the server evaluator already skips such objects — this guard makes
513    /// the invariant hold by construction rather than by the caller
514    /// remembering, so a future caller cannot reintroduce the violation.
515    fn set_event_state_internal(&mut self, state: EventState) -> Result<(), Error> {
516        if !self.event_detection_enable && state != EventState::NORMAL {
517            return Err(common::write_access_denied_error());
518        }
519        self.event_state = state;
520        Ok(())
521    }
522
523    /// Snapshot the pending countdown and algorithm baselines for the server
524    /// evaluator.
525    fn enrollment_eval_state_internal(&self) -> Option<EventEnrollmentEvalState> {
526        Some(EventEnrollmentEvalState {
527            pending: self.pending.clone(),
528            cov_baseline: self.cov_baseline.clone(),
529            last_offnormal_value: self.last_offnormal_value,
530        })
531    }
532
533    /// Store the enrollment evaluation state. Refused while
534    /// `Event_Detection_Enable` is FALSE: Clause 13.2.2.1 freezes the state
535    /// machine by suspending evaluation, and the reset in the
536    /// write arm has already returned these fields to their initial
537    /// condition, so a write arriving while disabled can only be stale.
538    fn set_enrollment_eval_state_internal(
539        &mut self,
540        state: EventEnrollmentEvalState,
541    ) -> Result<(), Error> {
542        if !self.event_detection_enable {
543            return Err(common::write_access_denied_error());
544        }
545        self.pending = state.pending;
546        self.cov_baseline = state.cov_baseline;
547        self.last_offnormal_value = state.last_offnormal_value;
548        Ok(())
549    }
550
551    fn enrollment_eval_source_internal(&self) -> Option<Option<EventEnrollmentMonitoredSource>> {
552        Some(self.monitored_reference)
553    }
554
555    fn set_enrollment_eval_source_internal(
556        &mut self,
557        source: Option<EventEnrollmentMonitoredSource>,
558    ) -> Result<(), Error> {
559        if !self.event_detection_enable {
560            return Err(common::write_access_denied_error());
561        }
562        self.monitored_reference = source;
563        Ok(())
564    }
565
566    /// Acknowledge an alarm transition (the AcknowledgeAlarm service route,
567    /// Clause 13.9): Clause 13.2.3 sets the bit on the acknowledgment
568    /// indication — unconditional and idempotent, so a repeated ack succeeds
569    /// again. A detection-DISABLED enrollment instead refuses with
570    /// OBJECT/NO_ALARM_CONFIGURED, which Table 13-10 uses for an existing object
571    /// lacking event-generation support or configuration: it can
572    /// generate nothing, and Clause 12.12 keeps its `Acked_Transitions` at
573    /// the initial condition, which an accepted ack would break.
574    /// Out_Of_Service does not gate the ack: no clause bars acknowledging a
575    /// notification already issued while the object is out of service.
576    fn acknowledge_alarm(
577        &mut self,
578        transition_bit: EventTransitionBits,
579    ) -> Result<(), bacnet_types::error::Error> {
580        if !self.event_detection_enable {
581            return Err(bacnet_types::error::Error::protocol(
582                bacnet_types::enums::ErrorClass::OBJECT,
583                bacnet_types::enums::ErrorCode::NO_ALARM_CONFIGURED,
584            ));
585        }
586        self.acked_transitions |= transition_bit & EventTransitionBits::all();
587        Ok(())
588    }
589
590    fn acknowledge_alarm_correlated_internal(
591        &mut self,
592        event_state: EventState,
593        timestamp: &BACnetTimeStamp,
594    ) -> Result<(), Error> {
595        if !self.event_detection_enable || self.enrollment_summary_capability_internal().is_none() {
596            return Err(Error::protocol(
597                ErrorClass::OBJECT,
598                ErrorCode::NO_ALARM_CONFIGURED,
599            ));
600        }
601        self.event_history.acknowledge_correlated(
602            &mut self.acked_transitions,
603            event_state,
604            timestamp,
605        )
606    }
607
608    fn acknowledge_alarm_correlated_detailed_internal(
609        &mut self,
610        event_state: EventState,
611        timestamp: &BACnetTimeStamp,
612    ) -> Result<Option<crate::event::EventStateChange>, Error> {
613        if !self.event_detection_enable || self.enrollment_summary_capability_internal().is_none() {
614            return Err(Error::protocol(
615                ErrorClass::OBJECT,
616                ErrorCode::NO_ALARM_CONFIGURED,
617            ));
618        }
619        self.event_history.acknowledge_correlated_detailed(
620            &mut self.acked_transitions,
621            event_state,
622            timestamp,
623        )
624    }
625
626    /// Clause 13.2.3's transition-received maintenance of `Acked_Transitions`:
627    /// the evaluator resolves `Ack_Required` from the referenced Notification
628    /// Class object and this call applies the outcome — clear the bit when
629    /// ack is required, set it otherwise. Refused while detection is
630    /// disabled, preserving the same invariant as above: Acked_Transitions
631    /// must retain its initial value throughout the disabled period.
632    fn set_acked_transitions_internal(
633        &mut self,
634        transition_bit: EventTransitionBits,
635        acknowledged: bool,
636    ) -> Result<(), Error> {
637        if !self.event_detection_enable {
638            return Err(common::write_access_denied_error());
639        }
640        self.acked_transitions
641            .set(transition_bit & EventTransitionBits::all(), acknowledged);
642        Ok(())
643    }
644
645    transition::impl_event_enrollment_transition_commit!();
646
647    /// Mirrors the `write_property` arms above, so PICS reports what dispatch
648    /// actually accepts.
649    ///
650    /// Enumerated explicitly: the intrinsic-reporting objects accept
651    /// `HIGH_LIMIT`, `LOW_LIMIT`, `DEADBAND`, `LIMIT_ENABLE` and `TIME_DELAY`,
652    /// none of which an Event Enrollment accepts, because it carries those
653    /// inside `Event_Parameters` instead. `TIME_DELAY_NORMAL` is accepted here
654    /// as a real (O-coded) property, per Table 12-14.
655    ///
656    /// `Event_Detection_Enable` is writable even though Table 12-14 codes it R
657    /// rather than W: Clause 12.1.2 lets implementors accept writes to an R
658    /// property unless that object's property description expressly forbids
659    /// them. Clause 12.12 has no such prohibition; it anticipates setting the
660    /// value during configuration without mandating that timing. Annex K's
661    /// AE-AVM-A BIBB (Table K-17) positively requires a conforming workstation
662    /// to be able to *write* this property, so refusing the write would be
663    /// interoperably hostile.
664    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
665        // Table 12-14 has no Out_Of_Service (#1064), so of the common
666        // writable rows only the name and description apply.
667        matches!(
668            property,
669            PropertyIdentifier::OBJECT_NAME
670                | PropertyIdentifier::DESCRIPTION
671                | PropertyIdentifier::NOTIFY_TYPE
672                | PropertyIdentifier::NOTIFICATION_CLASS
673                | PropertyIdentifier::EVENT_ENABLE
674                | PropertyIdentifier::EVENT_DETECTION_ENABLE
675                | PropertyIdentifier::EVENT_PARAMETERS
676                | PropertyIdentifier::FAULT_PARAMETERS
677                | PropertyIdentifier::TIME_DELAY_NORMAL
678                | PropertyIdentifier::EVENT_MESSAGE_TEXTS_CONFIG
679                | PropertyIdentifier::EVENT_ALGORITHM_INHIBIT_REF
680                | PropertyIdentifier::EVENT_ALGORITHM_INHIBIT
681        )
682    }
683
684    fn event_algorithm_inhibit_reference_internal(
685        &self,
686    ) -> Option<bacnet_types::constructed::BACnetObjectPropertyReference> {
687        self.event_history.options.inhibit_reference().cloned()
688    }
689
690    fn follow_event_algorithm_inhibit_internal(&mut self, inhibit: bool) -> bool {
691        self.event_history.options.follow(inhibit)
692    }
693
694    fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
695        Cow::Borrowed(metadata::EVENT_ENROLLMENT_PROPERTIES)
696    }
697
698    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
699        crate::property_metadata::property_list_from_metadata(metadata::EVENT_ENROLLMENT_PROPERTIES)
700    }
701}
702
703#[cfg(test)]
704mod tests;