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;