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