Skip to main content

bacnet_objects/access_control/
door.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use super::door_alarm::{DoorAlarmLists, LISTED_STATES};
5use super::door_out_of_service::DoorState;
6use super::*;
7use crate::event::state_reporting::ChangeOfStateReporting;
8use crate::traits::MonotonicClock;
9
10// AccessDoorObject (type 30)
11// ---------------------------------------------------------------------------
12
13/// Door_Pulse_Time a new door starts with, in tenths of a second (5 s).
14pub const DEFAULT_DOOR_PULSE_TIME: u32 = 50;
15/// Door_Extended_Pulse_Time a new door starts with, in tenths of a second
16/// (15 s).
17pub const DEFAULT_DOOR_EXTENDED_PULSE_TIME: u32 = 150;
18/// Door_Open_Too_Long_Time a new door starts with, in tenths of a second
19/// (30 s).
20pub const DEFAULT_DOOR_OPEN_TOO_LONG_TIME: u32 = 300;
21
22/// BACnet Access Door object (type 30).
23///
24/// Represents a physical door or barrier in an access control system.
25/// Present value carries the door command (BACnetDoorValue); Door_Status
26/// reports the physical DoorStatus.
27///
28/// A PULSE_UNLOCK or EXTENDED_PULSE_UNLOCK command holds its priority slot
29/// for Door_Pulse_Time or Door_Extended_Pulse_Time and is then relinquished,
30/// so the door relocks (Clauses 12.26.4, 12.26.16 and 12.26.17; #1073). The
31/// deadline is taken from the database's monotonic clock when the command is
32/// written, and the server's monotonic operation task relinquishes the slot
33/// at that deadline, the mechanism Binary Lighting Output egress uses. A
34/// pulse written below a priority already commanded is relinquished at once,
35/// and a pulse time of zero relinquishes at once too. Changing a pulse time
36/// leaves an armed deadline as it was.
37///
38/// While Out_Of_Service is TRUE a client can simulate Door_Status,
39/// Lock_Status and Door_Alarm_State by writing them, and the door's own values
40/// come back on the return to service (Clause 12.26.9, Table 12-30 footnote 1;
41/// #1131). The module `door_out_of_service` has the details.
42///
43/// Secured_Status isn't stored: each read works it out from what the door
44/// serves (Clause 12.26.14; #1148), so it moves with every command, relock,
45/// device report and simulated value. See `secured_status` for how the
46/// inputs combine and when the answer is UNKNOWN.
47///
48/// The door reports intrinsically on Door_Alarm_State with the
49/// CHANGE_OF_STATE algorithm (Clause 12.26; #1149): Event_State goes
50/// OFFNORMAL once Door_Alarm_State has stayed in Alarm_Values for Time_Delay
51/// seconds and back to NORMAL once it has stayed out of them for
52/// Time_Delay_Normal. Fault_Values feeds the FAULT_STATE algorithm, so a
53/// Door_Alarm_State among them makes Reliability MULTI_STATE_FAULT and
54/// Event_State FAULT. Masked_Alarm_Values keeps Door_Alarm_State out of the
55/// states it lists. The module `door_alarm` has the rules the three lists
56/// set; the event rows are served and written through
57/// `ChangeOfStateReporting`, and the server sends the notifications to the
58/// Notification Class recipients.
59///
60/// The application decides when the door is in alarm and reports it with
61/// [`Self::set_door_alarm_state`]; Clause 12.26.20 leaves that to the
62/// device. That includes DOOR_OPEN_TOO_LONG: the door serves
63/// Door_Open_Too_Long_Time for the application's logic to read but runs no
64/// timer of its own.
65#[derive(Clone)]
66pub struct AccessDoorObject {
67    oid: ObjectIdentifier,
68    name: String,
69    description: String,
70    present_value: DoorValue,
71    /// Door_Status, Lock_Status and Door_Alarm_State as served: the device's,
72    /// or a client's simulation while Out_Of_Service is TRUE.
73    state: DoorState,
74    /// The device's own door state, put aside while Out_Of_Service is TRUE.
75    device_state: Option<DoorState>,
76    door_members: Vec<BACnetDeviceObjectReference>,
77    status_flags: StatusFlags,
78    out_of_service: bool,
79    /// Fault_Values and Masked_Alarm_Values.
80    alarm_lists: DoorAlarmLists,
81    /// Intrinsic reporting on Door_Alarm_State, Alarm_Values included.
82    reporting: ChangeOfStateReporting,
83    /// 16-level priority array for commandable Present_Value.
84    priority_array: [Option<DoorValue>; 16],
85    relinquish_default: DoorValue,
86    /// Door_Pulse_Time, tenths of a second.
87    door_pulse_time: u32,
88    /// Door_Extended_Pulse_Time, tenths of a second.
89    door_extended_pulse_time: u32,
90    /// Door_Open_Too_Long_Time, tenths of a second. Stored and served for
91    /// the application, which decides when the door has been open too long.
92    door_open_too_long_time: u32,
93    /// For each priority slot holding a pulse, the monotonic instant it is
94    /// relinquished.
95    pulse_deadlines: [Option<Duration>; 16],
96    monotonic_clock: Option<Arc<MonotonicClock>>,
97    /// Elapsed time for a door with no bound clock, advanced by
98    /// `advance_time_internal`.
99    logical_now: Duration,
100}
101
102impl AccessDoorObject {
103    /// Create a new Access Door object.
104    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
105        let oid = ObjectIdentifier::new(ObjectType::ACCESS_DOOR, instance)?;
106        Ok(Self {
107            oid,
108            name: name.into(),
109            description: String::new(),
110            present_value: DoorValue::LOCK,
111            state: DoorState::SECURE,
112            device_state: None,
113            door_members: Vec::new(),
114            status_flags: StatusFlags::empty(),
115            out_of_service: false,
116            alarm_lists: DoorAlarmLists::default(),
117            reporting: ChangeOfStateReporting::new(LISTED_STATES),
118            priority_array: Default::default(),
119            relinquish_default: DoorValue::LOCK,
120            door_pulse_time: DEFAULT_DOOR_PULSE_TIME,
121            door_extended_pulse_time: DEFAULT_DOOR_EXTENDED_PULSE_TIME,
122            door_open_too_long_time: DEFAULT_DOOR_OPEN_TOO_LONG_TIME,
123            pulse_deadlines: [None; 16],
124            monotonic_clock: None,
125            logical_now: Duration::ZERO,
126        })
127    }
128
129    /// Set the Relinquish_Default (#270).
130    ///
131    /// Table 12-30 types it as BACnetDoorValue, but Clause 12.26.11 admits
132    /// only LOCK and UNLOCK: a pulse can't be the resting command. Any other
133    /// value, including a `DoorValue::from_raw` outside the production, is
134    /// refused with VALUE_OUT_OF_RANGE and the stored default kept. After the
135    /// store, Present_Value is resolved anew from the priority array so an
136    /// empty array falls back to the new default immediately.
137    pub fn set_relinquish_default(&mut self, value: DoorValue) -> Result<(), Error> {
138        if !matches!(value, DoorValue::LOCK | DoorValue::UNLOCK) {
139            return Err(common::value_out_of_range_error());
140        }
141        self.relinquish_default = value;
142        self.recalculate_present_value();
143        Ok(())
144    }
145
146    /// Set Door_Members, the objects that make up the physical door: its
147    /// lock, contact, reader and anything else the application chooses
148    /// (Clause 12.26.15 leaves the choice to the device). A reference with no
149    /// Device identifier names an object in this device. The array is
150    /// read-only over the network.
151    ///
152    /// A reference whose device identifier isn't a Device object is refused
153    /// with VALUE_OUT_OF_RANGE and the members set before are kept (#1285).
154    pub fn set_door_members(
155        &mut self,
156        members: impl IntoIterator<Item = impl Into<BACnetDeviceObjectReference>>,
157    ) -> Result<(), Error> {
158        let members: Vec<BACnetDeviceObjectReference> =
159            members.into_iter().map(Into::into).collect();
160        crate::device_reference::check_device_members(&members)?;
161        self.door_members = members;
162        Ok(())
163    }
164
165    /// Set Door_Pulse_Time, in tenths of a second.
166    pub fn set_door_pulse_time(&mut self, tenths: u32) {
167        self.door_pulse_time = tenths;
168    }
169
170    /// Set Door_Extended_Pulse_Time, in tenths of a second.
171    pub fn set_door_extended_pulse_time(&mut self, tenths: u32) {
172        self.door_extended_pulse_time = tenths;
173    }
174
175    /// Set Door_Open_Too_Long_Time, in tenths of a second.
176    pub fn set_door_open_too_long_time(&mut self, tenths: u32) {
177        self.door_open_too_long_time = tenths;
178    }
179
180    /// Set Door_Status, the open or closed state the door's contact reports.
181    ///
182    /// While Out_Of_Service is TRUE a client's simulated value keeps being
183    /// served, and this one takes over on the return to service.
184    pub fn set_door_status(&mut self, status: DoorStatus) {
185        self.device_state_mut().door_status = status;
186    }
187
188    /// Set Lock_Status, the state the door lock's monitor reports.
189    ///
190    /// While Out_Of_Service is TRUE a client's simulated value keeps being
191    /// served, and this one takes over on the return to service.
192    pub fn set_lock_status(&mut self, status: LockStatus) {
193        self.device_state_mut().lock_status = status;
194    }
195
196    /// Set Door_Alarm_State, the alarm condition the application's door
197    /// logic has worked out (Clause 12.26.20 leaves that to the device),
198    /// DOOR_OPEN_TOO_LONG included.
199    ///
200    /// The state has to be NORMAL or a member of Alarm_Values or
201    /// Fault_Values, and no member of Masked_Alarm_Values; any other is
202    /// refused with VALUE_OUT_OF_RANGE and the state held is kept. A change
203    /// of the value served triggers a SubscribeCOV notification (Table 13-1)
204    /// and feeds the event algorithm, which a running server evaluates at its
205    /// next one-second tick. While Out_Of_Service is TRUE a client's
206    /// simulated value keeps being served, and this one takes over on the
207    /// return to service.
208    pub fn set_door_alarm_state(&mut self, state: DoorAlarmState) -> Result<(), Error> {
209        if !self.admits(state) {
210            return Err(common::value_out_of_range_error());
211        }
212        self.device_state_mut().door_alarm_state = state;
213        Ok(())
214    }
215
216    /// Set Alarm_Values, the Door_Alarm_State values the door reports as
217    /// offnormal (Clause 12.26.25). NORMAL, or a value outside the
218    /// BACnetDoorAlarmState production, named or proprietary (256 to
219    /// 65535), is refused with VALUE_OUT_OF_RANGE and the values set before
220    /// are kept. A Door_Alarm_State the lists no longer admit drops to
221    /// NORMAL. Clients can write the list too, as they can the door's other
222    /// event configuration.
223    pub fn set_alarm_values(
224        &mut self,
225        states: impl IntoIterator<Item = DoorAlarmState>,
226    ) -> Result<(), Error> {
227        self.reporting.set_alarm_values(raw_states(states))?;
228        self.settle_door_alarm_state();
229        Ok(())
230    }
231
232    /// Set Fault_Values, the Door_Alarm_State values that make Reliability
233    /// MULTI_STATE_FAULT (Clause 12.26.26), checked as
234    /// [`Self::set_alarm_values`] checks its list.
235    pub fn set_fault_values(
236        &mut self,
237        states: impl IntoIterator<Item = DoorAlarmState>,
238    ) -> Result<(), Error> {
239        self.alarm_lists.set_fault_values(raw_states(states))?;
240        self.settle_door_alarm_state();
241        Ok(())
242    }
243
244    /// Set Masked_Alarm_Values, the states Door_Alarm_State is kept out of
245    /// (Clause 12.26.21), checked as [`Self::set_alarm_values`] checks its
246    /// list. A door in a state the list now masks returns to NORMAL at once.
247    pub fn set_masked_alarm_values(
248        &mut self,
249        states: impl IntoIterator<Item = DoorAlarmState>,
250    ) -> Result<(), Error> {
251        self.alarm_lists
252            .set_masked_alarm_values(raw_states(states))?;
253        self.settle_door_alarm_state();
254        Ok(())
255    }
256
257    /// Whether the door's lists let Door_Alarm_State take `state`.
258    fn admits(&self, state: DoorAlarmState) -> bool {
259        self.alarm_lists
260            .admits(self.reporting.alarm_values(), state)
261    }
262
263    /// Send a Door_Alarm_State the lists no longer admit back to NORMAL,
264    /// both the one served and the device's own one put aside out of
265    /// service.
266    fn settle_door_alarm_state(&mut self) {
267        let (lists, alarm_values) = (&self.alarm_lists, self.reporting.alarm_values());
268        for state in std::iter::once(&mut self.state).chain(self.device_state.as_mut()) {
269            if !lists.admits(alarm_values, state.door_alarm_state) {
270                state.door_alarm_state = DoorAlarmState::NORMAL;
271            }
272        }
273    }
274
275    /// The value the event algorithm watches: Door_Alarm_State, raw.
276    fn watched_alarm_state(&self) -> u32 {
277        self.state.door_alarm_state.to_raw()
278    }
279
280    /// Reliability as served: a client's simulated value while out of
281    /// service, else what the FAULT_STATE algorithm makes of the
282    /// Door_Alarm_State served.
283    fn served_reliability(&self) -> Reliability {
284        self.state
285            .reliability
286            .unwrap_or_else(|| self.alarm_lists.fault_state(self.state.door_alarm_state))
287    }
288
289    /// The device's own door state: the one put aside while out of service,
290    /// else the one served.
291    fn device_state_mut(&mut self) -> &mut DoorState {
292        self.device_state.as_mut().unwrap_or(&mut self.state)
293    }
294
295    fn recalculate_present_value(&mut self) {
296        self.present_value =
297            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
298    }
299
300    fn monotonic_now(&self) -> Duration {
301        self.monotonic_clock
302            .as_ref()
303            .map_or(self.logical_now, |clock| clock())
304    }
305
306    /// Store a command (or a NULL relinquish) at `priority`, arming the
307    /// relock deadline for a pulse.
308    fn command(&mut self, priority: u8, value: Option<DoorValue>) {
309        let index = usize::from(priority - 1);
310        self.pulse_deadlines[index] = None;
311        let pulse = match value {
312            Some(DoorValue::PULSE_UNLOCK) => Some(self.door_pulse_time),
313            Some(DoorValue::EXTENDED_PULSE_UNLOCK) => Some(self.door_extended_pulse_time),
314            _ => None,
315        };
316        self.priority_array[index] = match pulse {
317            // A pulse below a live command, or of zero length, is
318            // relinquished as soon as it is written.
319            Some(tenths)
320                if tenths == 0 || self.priority_array[..index].iter().any(Option::is_some) =>
321            {
322                None
323            }
324            Some(tenths) => {
325                let length = Duration::from_millis(u64::from(tenths) * 100);
326                self.pulse_deadlines[index] = Some(self.monotonic_now().saturating_add(length));
327                value
328            }
329            None => value,
330        };
331        self.recalculate_present_value();
332    }
333
334    /// Relinquish every pulse whose deadline has passed; `true` when one was.
335    fn expire_pulses(&mut self, now: Duration) -> bool {
336        let mut expired = false;
337        for (slot, deadline) in self
338            .priority_array
339            .iter_mut()
340            .zip(&mut self.pulse_deadlines)
341        {
342            if deadline.is_some_and(|deadline| now >= deadline) {
343                *slot = None;
344                *deadline = None;
345                expired = true;
346            }
347        }
348        if expired {
349            self.recalculate_present_value();
350        }
351        expired
352    }
353
354    /// Whether Status_Flags carries IN_ALARM, which follows Event_State
355    /// (Clause 12.26.6). Status_Flags and Secured_Status both read it here,
356    /// so the two agree.
357    fn in_alarm(&self) -> bool {
358        self.reporting.event_state() != EventState::NORMAL
359    }
360
361    /// Secured_Status, worked out from what the door serves (Clause
362    /// 12.26.14).
363    ///
364    /// Each input comes out met, failed or undetermined. One failed input
365    /// makes the door UNSECURED whatever the rest say: a door commanded to
366    /// UNLOCK isn't secure even while its contact can't tell whether it is
367    /// shut. With none failed, one undetermined input makes it UNKNOWN, and
368    /// only a door with every input met is SECURED.
369    ///
370    /// An input is undetermined only when the door's own monitor says it
371    /// can't tell: Door_Status or Lock_Status reads UNKNOWN, or reports its
372    /// input faulted (DOOR_FAULT, LOCK_FAULT). Reliability is no input of its
373    /// own: a fault moves Event_State to FAULT, which sets IN_ALARM and so
374    /// fails the first input. Door_Status and Lock_Status are the served
375    /// values, so while Out_Of_Service is TRUE a client's simulation moves
376    /// the result as a device report would (Clause 12.26.9).
377    fn secured_status(&self) -> DoorSecuredStatus {
378        let inputs = [
379            Some(!self.in_alarm()),
380            // Any masked state fails the door, whichever it is.
381            Some(!self.alarm_lists.masks_any()),
382            Some(self.present_value == DoorValue::LOCK),
383            door_closed(self.state.door_status),
384            door_locked(self.state.lock_status),
385        ];
386        if inputs.contains(&Some(false)) {
387            DoorSecuredStatus::UNSECURED
388        } else if inputs.contains(&None) {
389            DoorSecuredStatus::UNKNOWN
390        } else {
391            DoorSecuredStatus::SECURED
392        }
393    }
394}
395
396/// Door_Status as a Secured_Status input: met for a shut door or one with no
397/// contact fitted (CLOSED, UNUSED), undetermined (`None`) while the contact
398/// can't tell or is faulted (UNKNOWN, DOOR_FAULT), and failed for any other
399/// value, a proprietary one included.
400fn door_closed(status: DoorStatus) -> Option<bool> {
401    match status {
402        DoorStatus::CLOSED | DoorStatus::UNUSED => Some(true),
403        DoorStatus::UNKNOWN | DoorStatus::DOOR_FAULT => None,
404        _ => Some(false),
405    }
406}
407
408/// Lock_Status as a Secured_Status input, read the same way: met for LOCKED
409/// or UNUSED, undetermined for UNKNOWN or LOCK_FAULT, failed for UNLOCKED.
410fn door_locked(status: LockStatus) -> Option<bool> {
411    match status {
412        LockStatus::LOCKED | LockStatus::UNUSED => Some(true),
413        LockStatus::UNKNOWN | LockStatus::LOCK_FAULT => None,
414        _ => Some(false),
415    }
416}
417
418/// Whether `value` is one of the four BACnetDoorValue members. The
419/// bacnet-types test `door_value_values_match_clause_21` pins the
420/// production's closed-set length, so this bound cannot drift from the enum.
421fn is_door_value(value: DoorValue) -> bool {
422    value.to_raw() <= DoorValue::EXTENDED_PULSE_UNLOCK.to_raw()
423}
424
425/// Decode a BACnetDoorValue write: an Enumerated within the closed set.
426fn checked_door_value(value: PropertyValue) -> Result<DoorValue, Error> {
427    let PropertyValue::Enumerated(raw) = value else {
428        return Err(common::invalid_data_type_error());
429    };
430    let value = DoorValue::from_raw(raw);
431    if !is_door_value(value) {
432        return Err(common::value_out_of_range_error());
433    }
434    Ok(value)
435}
436
437/// Decode a door time write: an Unsigned of at most 32 bits.
438fn checked_tenths(value: PropertyValue) -> Result<u32, Error> {
439    let PropertyValue::Unsigned(raw) = value else {
440        return Err(common::invalid_data_type_error());
441    };
442    common::u64_to_u32(raw)
443}
444
445impl BACnetObject for AccessDoorObject {
446    fn object_identifier(&self) -> ObjectIdentifier {
447        self.oid
448    }
449
450    fn object_name(&self) -> &str {
451        &self.name
452    }
453
454    fn read_property(
455        &self,
456        property: PropertyIdentifier,
457        array_index: Option<u32>,
458    ) -> Result<PropertyValue, Error> {
459        if let Some(result) = common::read_identity_properties!(self, property, array_index) {
460            return result;
461        }
462        if let Some(result) = self.reporting.read(property, array_index) {
463            return result;
464        }
465        if let Some(value) = self.alarm_lists.read(property) {
466            return Ok(value);
467        }
468        match property {
469            p if p == PropertyIdentifier::OBJECT_TYPE => {
470                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_DOOR.to_raw()))
471            }
472            // IN_ALARM comes from the door's Event_State, the flag
473            // Secured_Status reads, and FAULT from the Reliability served.
474            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
475                self.status_flags,
476                self.served_reliability(),
477                self.out_of_service,
478                self.reporting.event_state(),
479            )),
480            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
481                Ok(PropertyValue::Boolean(self.out_of_service))
482            }
483            p if p == PropertyIdentifier::RELIABILITY => Ok(PropertyValue::Enumerated(
484                self.served_reliability().to_raw(),
485            )),
486            p if p == PropertyIdentifier::PRESENT_VALUE => {
487                Ok(PropertyValue::Enumerated(self.present_value.to_raw()))
488            }
489            p if p == PropertyIdentifier::DOOR_STATUS => {
490                Ok(PropertyValue::Enumerated(self.state.door_status.to_raw()))
491            }
492            p if p == PropertyIdentifier::LOCK_STATUS => {
493                Ok(PropertyValue::Enumerated(self.state.lock_status.to_raw()))
494            }
495            p if p == PropertyIdentifier::SECURED_STATUS => {
496                Ok(PropertyValue::Enumerated(self.secured_status().to_raw()))
497            }
498            p if p == PropertyIdentifier::DOOR_ALARM_STATE => Ok(PropertyValue::Enumerated(
499                self.state.door_alarm_state.to_raw(),
500            )),
501            p if p == PropertyIdentifier::DOOR_MEMBERS => common::read_array(
502                crate::device_reference::reference_elements(&self.door_members),
503                array_index,
504            ),
505            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
506                common::read_priority_array!(self, array_index, |v: DoorValue| {
507                    PropertyValue::Enumerated(v.to_raw())
508                })
509            }
510            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
511                Ok(PropertyValue::Enumerated(self.relinquish_default.to_raw()))
512            }
513            p if p == PropertyIdentifier::DOOR_PULSE_TIME => {
514                Ok(PropertyValue::Unsigned(self.door_pulse_time.into()))
515            }
516            p if p == PropertyIdentifier::DOOR_EXTENDED_PULSE_TIME => Ok(PropertyValue::Unsigned(
517                self.door_extended_pulse_time.into(),
518            )),
519            p if p == PropertyIdentifier::DOOR_OPEN_TOO_LONG_TIME => {
520                Ok(PropertyValue::Unsigned(self.door_open_too_long_time.into()))
521            }
522            // NULL while Present_Value comes from Relinquish_Default
523            // (Clause 12.26.39).
524            p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
525                Ok(common::current_command_priority(&self.priority_array))
526            }
527            _ => Err(common::unknown_property_error()),
528        }
529    }
530
531    fn write_property(
532        &mut self,
533        property: PropertyIdentifier,
534        array_index: Option<u32>,
535        value: PropertyValue,
536        priority: Option<u8>,
537    ) -> Result<(), Error> {
538        // The door's entry edge is always seen (it starts in service and only
539        // this write moves Out_Of_Service), so there is no fallback state.
540        if let Some(result) = common::write_out_of_service_with_restore(
541            &mut self.out_of_service,
542            &mut self.state,
543            &mut self.device_state,
544            None,
545            property,
546            &value,
547        ) {
548            return result;
549        }
550        if let Some(result) = common::write_description(&mut self.description, property, &value) {
551            return result;
552        }
553        let (lists, alarm_values) = (&self.alarm_lists, self.reporting.alarm_values());
554        if let Some(result) = self.state.write(
555            self.out_of_service,
556            |state| lists.admits(alarm_values, state),
557            property,
558            &value,
559        ) {
560            return result;
561        }
562        // An edit of any of the three lists can leave Door_Alarm_State in a
563        // state they no longer admit.
564        let list_edit = match property {
565            p if p == PropertyIdentifier::ALARM_VALUES => {
566                self.reporting.write(property, array_index, &value)
567            }
568            _ => self.alarm_lists.write(property, array_index, &value),
569        };
570        if let Some(result) = list_edit {
571            result?;
572            self.settle_door_alarm_state();
573            return Ok(());
574        }
575        if let Some(result) = self.reporting.write(property, array_index, &value) {
576            return result;
577        }
578        match property {
579            // A command (or a NULL relinquish) at the write priority, 16 when
580            // absent; Present_Value is then resolved from the priority array.
581            p if p == PropertyIdentifier::PRESENT_VALUE => {
582                let priority = priority.unwrap_or(16);
583                if !(1..=16).contains(&priority) {
584                    return Err(common::value_out_of_range_error());
585                }
586                let value = match value {
587                    PropertyValue::Null => None,
588                    other => Some(checked_door_value(other)?),
589                };
590                self.command(priority, value);
591                Ok(())
592            }
593            // Table 12-30 carries Relinquish_Default R (BACnetDoorValue) for
594            // the commandable Access Door; the standard permits writability.
595            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
596                self.set_relinquish_default(checked_door_value(value)?)
597            }
598            p if p == PropertyIdentifier::DOOR_PULSE_TIME => {
599                self.door_pulse_time = checked_tenths(value)?;
600                Ok(())
601            }
602            p if p == PropertyIdentifier::DOOR_EXTENDED_PULSE_TIME => {
603                self.door_extended_pulse_time = checked_tenths(value)?;
604                Ok(())
605            }
606            p if p == PropertyIdentifier::DOOR_OPEN_TOO_LONG_TIME => {
607                self.door_open_too_long_time = checked_tenths(value)?;
608                Ok(())
609            }
610            _ => Err(crate::common::unhandled_write_error(
611                self.property_metadata().as_ref(),
612                property,
613                array_index,
614            )),
615        }
616    }
617
618    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
619        super::metadata_topology::for_access_door_object(self)
620    }
621
622    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
623        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
624    }
625
626    fn supports_cov(&self) -> bool {
627        true
628    }
629
630    crate::event::impl_change_of_state_reporting!(
631        reporting,
632        Self::watched_alarm_state,
633        Self::served_reliability
634    );
635
636    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
637        self.logical_now = self.logical_now.saturating_add(elapsed);
638        self.expire_pulses(self.logical_now)
639    }
640
641    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
642        self.monotonic_clock = clock;
643    }
644
645    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
646        self.expire_pulses(now)
647    }
648
649    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
650        self.pulse_deadlines.iter().flatten().min().copied()
651    }
652
653    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
654        Some(Box::new(self.clone()))
655    }
656}
657
658/// The raw values of `states`, for the list setters.
659fn raw_states(states: impl IntoIterator<Item = DoorAlarmState>) -> Vec<u32> {
660    states.into_iter().map(DoorAlarmState::to_raw).collect()
661}
662
663#[cfg(test)]
664#[path = "door_secured_status_tests.rs"]
665mod secured_status_tests;
666
667// ---------------------------------------------------------------------------