Skip to main content

bacnet_objects/access_control/
door.rs

1use super::*;
2
3// AccessDoorObject (type 30)
4// ---------------------------------------------------------------------------
5
6/// BACnet Access Door object (type 30).
7///
8/// Represents a physical door or barrier in an access control system.
9/// Present value carries the door command (BACnetDoorValue); Door_Status
10/// reports the physical DoorStatus.
11pub struct AccessDoorObject {
12    oid: ObjectIdentifier,
13    name: String,
14    description: String,
15    present_value: u32,    // BACnetDoorValue: 0=lock, 1=unlock
16    door_status: u32,      // DoorStatus enumeration
17    lock_status: u32,      // LockStatus enumeration
18    secured_status: u32,   // DoorSecuredStatus enumeration
19    door_alarm_state: u32, // DoorAlarmState enumeration
20    door_members: Vec<ObjectIdentifier>,
21    status_flags: StatusFlags,
22    /// Event_State: 0 = NORMAL.
23    event_state: u32,
24    out_of_service: bool,
25    reliability: u32,
26    /// 16-level priority array for commandable Present_Value.
27    priority_array: [Option<u32>; 16],
28    relinquish_default: u32,
29}
30
31struct AccessDoorWriteRollback {
32    priority_array: [Option<u32>; 16],
33}
34
35impl AccessDoorObject {
36    /// Create a new Access Door object.
37    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
38        let oid = ObjectIdentifier::new(ObjectType::ACCESS_DOOR, instance)?;
39        Ok(Self {
40            oid,
41            name: name.into(),
42            description: String::new(),
43            present_value: 0, // lock
44            door_status: 0,   // closed
45            lock_status: 0,
46            secured_status: 0,
47            door_alarm_state: 0,
48            door_members: Vec::new(),
49            status_flags: StatusFlags::empty(),
50            event_state: 0, // NORMAL
51            out_of_service: false,
52            reliability: 0,
53            priority_array: Default::default(),
54            relinquish_default: 0, // lock
55        })
56    }
57
58    /// Set the Relinquish_Default (#270).
59    ///
60    /// Table 12-30 types both Present_Value and Relinquish_Default as
61    /// BACnetDoorValue, whose Clause 21 production is a closed set of four:
62    /// the accepted domain is `DoorValue::LOCK..=DoorValue::EXTENDED_PULSE_UNLOCK`
63    /// (0..=3), matching what the priority-slot Present_Value arm accepts
64    /// for value 3. The bacnet-types test `door_value_values_match_clause_21`
65    /// pins the production's closed-set length, so this derivation cannot
66    /// silently drift from the enum. After the store, Present_Value is
67    /// resolved anew from the priority array so an empty array falls back
68    /// to the new default immediately.
69    pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
70        if value > DoorValue::EXTENDED_PULSE_UNLOCK.to_raw() {
71            return Err(common::value_out_of_range_error());
72        }
73        self.relinquish_default = value;
74        self.recalculate_present_value();
75        Ok(())
76    }
77
78    fn recalculate_present_value(&mut self) {
79        self.present_value = self
80            .priority_array
81            .iter()
82            .flatten()
83            .next()
84            .copied()
85            .unwrap_or(self.relinquish_default);
86    }
87}
88
89impl BACnetObject for AccessDoorObject {
90    fn object_identifier(&self) -> ObjectIdentifier {
91        self.oid
92    }
93
94    fn object_name(&self) -> &str {
95        &self.name
96    }
97
98    fn read_property(
99        &self,
100        property: PropertyIdentifier,
101        array_index: Option<u32>,
102    ) -> Result<PropertyValue, Error> {
103        if let Some(result) = read_common_properties!(self, property, array_index) {
104            return result;
105        }
106        match property {
107            p if p == PropertyIdentifier::OBJECT_TYPE => {
108                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_DOOR.to_raw()))
109            }
110            p if p == PropertyIdentifier::PRESENT_VALUE => {
111                Ok(PropertyValue::Enumerated(self.present_value))
112            }
113            p if p == PropertyIdentifier::DOOR_STATUS => {
114                Ok(PropertyValue::Enumerated(self.door_status))
115            }
116            p if p == PropertyIdentifier::LOCK_STATUS => {
117                Ok(PropertyValue::Enumerated(self.lock_status))
118            }
119            p if p == PropertyIdentifier::SECURED_STATUS => {
120                Ok(PropertyValue::Enumerated(self.secured_status))
121            }
122            p if p == PropertyIdentifier::DOOR_ALARM_STATE => {
123                Ok(PropertyValue::Enumerated(self.door_alarm_state))
124            }
125            p if p == PropertyIdentifier::DOOR_MEMBERS => Ok(PropertyValue::List(
126                self.door_members
127                    .iter()
128                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
129                    .collect(),
130            )),
131            p if p == PropertyIdentifier::EVENT_STATE => {
132                Ok(PropertyValue::Enumerated(self.event_state))
133            }
134            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
135                common::read_priority_array!(self, array_index, PropertyValue::Enumerated)
136            }
137            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
138                Ok(PropertyValue::Enumerated(self.relinquish_default))
139            }
140            _ => Err(common::unknown_property_error()),
141        }
142    }
143
144    fn write_property(
145        &mut self,
146        property: PropertyIdentifier,
147        _array_index: Option<u32>,
148        value: PropertyValue,
149        priority: Option<u8>,
150    ) -> Result<(), Error> {
151        if let Some(result) =
152            common::write_out_of_service(&mut self.out_of_service, property, &value)
153        {
154            return result;
155        }
156        if let Some(result) = common::write_description(&mut self.description, property, &value) {
157            return result;
158        }
159        match property {
160            p if p == PropertyIdentifier::PRESENT_VALUE => {
161                let slot = priority.unwrap_or(16).clamp(1, 16) as usize - 1;
162                if let PropertyValue::Null = value {
163                    // Relinquish command at this priority
164                    self.priority_array[slot] = None;
165                } else if let PropertyValue::Enumerated(v) = value {
166                    self.priority_array[slot] = Some(v);
167                } else if self.out_of_service {
168                    // When OOS, accept direct writes without priority
169                    if let PropertyValue::Enumerated(v) = value {
170                        self.present_value = v;
171                        return Ok(());
172                    }
173                    return Err(common::invalid_data_type_error());
174                } else {
175                    return Err(common::invalid_data_type_error());
176                }
177                // Recalculate PV from priority array
178                self.recalculate_present_value();
179                Ok(())
180            }
181            // Table 12-30 carries Relinquish_Default R (BACnetDoorValue) for
182            // the commandable Access Door; the standard permits writability.
183            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
184                if let PropertyValue::Enumerated(v) = value {
185                    return self.set_relinquish_default(v);
186                }
187                Err(common::invalid_data_type_error())
188            }
189            _ => Err(common::write_access_denied_error()),
190        }
191    }
192
193    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
194        static PROPS: &[PropertyIdentifier] = &[
195            PropertyIdentifier::OBJECT_IDENTIFIER,
196            PropertyIdentifier::OBJECT_NAME,
197            PropertyIdentifier::DESCRIPTION,
198            PropertyIdentifier::OBJECT_TYPE,
199            PropertyIdentifier::PRESENT_VALUE,
200            PropertyIdentifier::DOOR_STATUS,
201            PropertyIdentifier::LOCK_STATUS,
202            PropertyIdentifier::SECURED_STATUS,
203            PropertyIdentifier::DOOR_ALARM_STATE,
204            PropertyIdentifier::DOOR_MEMBERS,
205            PropertyIdentifier::STATUS_FLAGS,
206            PropertyIdentifier::OUT_OF_SERVICE,
207            PropertyIdentifier::RELIABILITY,
208        ];
209        Cow::Borrowed(PROPS)
210    }
211
212    fn capture_write_property_rollback(
213        &mut self,
214        property: PropertyIdentifier,
215        _value: &PropertyValue,
216    ) -> Option<crate::traits::WritePropertyRollback> {
217        (property == PropertyIdentifier::PRESENT_VALUE).then(|| {
218            crate::traits::WritePropertyRollback::new(AccessDoorWriteRollback {
219                priority_array: self.priority_array,
220            })
221        })
222    }
223
224    fn restore_write_property_rollback(
225        &mut self,
226        rollback: crate::traits::WritePropertyRollback,
227    ) -> Result<(), Error> {
228        self.priority_array = rollback
229            .downcast::<AccessDoorWriteRollback>()?
230            .priority_array;
231        self.recalculate_present_value();
232        Ok(())
233    }
234
235    fn supports_cov(&self) -> bool {
236        true
237    }
238
239    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
240        // Mirrors the AccessDoorObject `write_property` arms so the PICS and
241        // runtime dispatch share one truth source.
242        matches!(
243            property,
244            PropertyIdentifier::OUT_OF_SERVICE
245                | PropertyIdentifier::DESCRIPTION
246                | PropertyIdentifier::PRESENT_VALUE
247                | PropertyIdentifier::RELINQUISH_DEFAULT
248        )
249    }
250}
251
252// ---------------------------------------------------------------------------