Skip to main content

bacnet_objects/
access_control.rs

1//! Access Control objects (ASHRAE 135-2020 Clause 12).
2//!
3//! This module implements the seven BACnet access control object types:
4//! - AccessDoor (type 30)
5//! - AccessCredential (type 32)
6//! - AccessPoint (type 33)
7//! - AccessRights (type 34)
8//! - AccessUser (type 35)
9//! - AccessZone (type 36)
10//! - CredentialDataInput (type 37)
11
12use bacnet_types::enums::{ObjectType, PropertyIdentifier};
13use bacnet_types::error::Error;
14use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
15use std::borrow::Cow;
16
17use crate::common::{self, read_common_properties};
18use crate::traits::BACnetObject;
19
20// ---------------------------------------------------------------------------
21// AccessDoorObject (type 30)
22// ---------------------------------------------------------------------------
23
24/// BACnet Access Door object (type 30).
25///
26/// Represents a physical door or barrier in an access control system.
27/// Present value indicates the door command status (DoorStatus enumeration).
28pub struct AccessDoorObject {
29    oid: ObjectIdentifier,
30    name: String,
31    description: String,
32    present_value: u32,    // DoorStatus: 0=closed, 1=opened, 2=unknown
33    door_status: u32,      // DoorStatus enumeration
34    lock_status: u32,      // LockStatus enumeration
35    secured_status: u32,   // DoorSecuredStatus enumeration
36    door_alarm_state: u32, // DoorAlarmState enumeration
37    door_members: Vec<ObjectIdentifier>,
38    status_flags: StatusFlags,
39    /// Event_State: 0 = NORMAL.
40    event_state: u32,
41    out_of_service: bool,
42    reliability: u32,
43    /// 16-level priority array for commandable Present_Value.
44    priority_array: [Option<u32>; 16],
45    relinquish_default: u32,
46}
47
48impl AccessDoorObject {
49    /// Create a new Access Door object.
50    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
51        let oid = ObjectIdentifier::new(ObjectType::ACCESS_DOOR, instance)?;
52        Ok(Self {
53            oid,
54            name: name.into(),
55            description: String::new(),
56            present_value: 0, // closed
57            door_status: 0,   // closed
58            lock_status: 0,
59            secured_status: 0,
60            door_alarm_state: 0,
61            door_members: Vec::new(),
62            status_flags: StatusFlags::empty(),
63            event_state: 0, // NORMAL
64            out_of_service: false,
65            reliability: 0,
66            priority_array: Default::default(),
67            relinquish_default: 0, // closed
68        })
69    }
70}
71
72impl BACnetObject for AccessDoorObject {
73    fn object_identifier(&self) -> ObjectIdentifier {
74        self.oid
75    }
76
77    fn object_name(&self) -> &str {
78        &self.name
79    }
80
81    fn read_property(
82        &self,
83        property: PropertyIdentifier,
84        array_index: Option<u32>,
85    ) -> Result<PropertyValue, Error> {
86        if let Some(result) = read_common_properties!(self, property, array_index) {
87            return result;
88        }
89        match property {
90            p if p == PropertyIdentifier::OBJECT_TYPE => {
91                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_DOOR.to_raw()))
92            }
93            p if p == PropertyIdentifier::PRESENT_VALUE => {
94                Ok(PropertyValue::Enumerated(self.present_value))
95            }
96            p if p == PropertyIdentifier::DOOR_STATUS => {
97                Ok(PropertyValue::Enumerated(self.door_status))
98            }
99            p if p == PropertyIdentifier::LOCK_STATUS => {
100                Ok(PropertyValue::Enumerated(self.lock_status))
101            }
102            p if p == PropertyIdentifier::SECURED_STATUS => {
103                Ok(PropertyValue::Enumerated(self.secured_status))
104            }
105            p if p == PropertyIdentifier::DOOR_ALARM_STATE => {
106                Ok(PropertyValue::Enumerated(self.door_alarm_state))
107            }
108            p if p == PropertyIdentifier::DOOR_MEMBERS => Ok(PropertyValue::List(
109                self.door_members
110                    .iter()
111                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
112                    .collect(),
113            )),
114            p if p == PropertyIdentifier::EVENT_STATE => {
115                Ok(PropertyValue::Enumerated(self.event_state))
116            }
117            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
118                common::read_priority_array!(self, array_index, PropertyValue::Enumerated)
119            }
120            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
121                Ok(PropertyValue::Enumerated(self.relinquish_default))
122            }
123            _ => Err(common::unknown_property_error()),
124        }
125    }
126
127    fn write_property(
128        &mut self,
129        property: PropertyIdentifier,
130        _array_index: Option<u32>,
131        value: PropertyValue,
132        priority: Option<u8>,
133    ) -> Result<(), Error> {
134        if let Some(result) =
135            common::write_out_of_service(&mut self.out_of_service, property, &value)
136        {
137            return result;
138        }
139        if let Some(result) = common::write_description(&mut self.description, property, &value) {
140            return result;
141        }
142        match property {
143            p if p == PropertyIdentifier::PRESENT_VALUE => {
144                let slot = priority.unwrap_or(16).clamp(1, 16) as usize - 1;
145                if let PropertyValue::Null = value {
146                    // Relinquish command at this priority
147                    self.priority_array[slot] = None;
148                } else if let PropertyValue::Enumerated(v) = value {
149                    self.priority_array[slot] = Some(v);
150                } else if self.out_of_service {
151                    // When OOS, accept direct writes without priority
152                    if let PropertyValue::Enumerated(v) = value {
153                        self.present_value = v;
154                        return Ok(());
155                    }
156                    return Err(common::invalid_data_type_error());
157                } else {
158                    return Err(common::invalid_data_type_error());
159                }
160                // Recalculate PV from priority array
161                self.present_value = self
162                    .priority_array
163                    .iter()
164                    .flatten()
165                    .next()
166                    .copied()
167                    .unwrap_or(self.relinquish_default);
168                Ok(())
169            }
170            _ => Err(common::write_access_denied_error()),
171        }
172    }
173
174    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
175        static PROPS: &[PropertyIdentifier] = &[
176            PropertyIdentifier::OBJECT_IDENTIFIER,
177            PropertyIdentifier::OBJECT_NAME,
178            PropertyIdentifier::DESCRIPTION,
179            PropertyIdentifier::OBJECT_TYPE,
180            PropertyIdentifier::PRESENT_VALUE,
181            PropertyIdentifier::DOOR_STATUS,
182            PropertyIdentifier::LOCK_STATUS,
183            PropertyIdentifier::SECURED_STATUS,
184            PropertyIdentifier::DOOR_ALARM_STATE,
185            PropertyIdentifier::DOOR_MEMBERS,
186            PropertyIdentifier::STATUS_FLAGS,
187            PropertyIdentifier::OUT_OF_SERVICE,
188            PropertyIdentifier::RELIABILITY,
189        ];
190        Cow::Borrowed(PROPS)
191    }
192
193    fn supports_cov(&self) -> bool {
194        true
195    }
196}
197
198// ---------------------------------------------------------------------------
199// AccessCredentialObject (type 32)
200// ---------------------------------------------------------------------------
201
202/// BACnet Access Credential object (type 32).
203///
204/// Represents a credential (card, fob, biometric, etc.) used for access control.
205/// Present value indicates active/inactive status (BinaryPV).
206pub struct AccessCredentialObject {
207    oid: ObjectIdentifier,
208    name: String,
209    description: String,
210    present_value: u32, // BinaryPV: 0=inactive, 1=active
211    credential_status: u32,
212    assigned_access_rights_count: u32,
213    authentication_factors: Vec<Vec<u8>>,
214    status_flags: StatusFlags,
215    out_of_service: bool,
216    reliability: u32,
217}
218
219impl AccessCredentialObject {
220    /// Create a new Access Credential object.
221    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
222        let oid = ObjectIdentifier::new(ObjectType::ACCESS_CREDENTIAL, instance)?;
223        Ok(Self {
224            oid,
225            name: name.into(),
226            description: String::new(),
227            present_value: 0, // inactive
228            credential_status: 0,
229            assigned_access_rights_count: 0,
230            authentication_factors: Vec::new(),
231            status_flags: StatusFlags::empty(),
232            out_of_service: false,
233            reliability: 0,
234        })
235    }
236}
237
238impl BACnetObject for AccessCredentialObject {
239    fn object_identifier(&self) -> ObjectIdentifier {
240        self.oid
241    }
242
243    fn object_name(&self) -> &str {
244        &self.name
245    }
246
247    fn read_property(
248        &self,
249        property: PropertyIdentifier,
250        array_index: Option<u32>,
251    ) -> Result<PropertyValue, Error> {
252        if let Some(result) = read_common_properties!(self, property, array_index) {
253            return result;
254        }
255        match property {
256            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
257                ObjectType::ACCESS_CREDENTIAL.to_raw(),
258            )),
259            p if p == PropertyIdentifier::PRESENT_VALUE => {
260                Ok(PropertyValue::Enumerated(self.present_value))
261            }
262            p if p == PropertyIdentifier::CREDENTIAL_STATUS => {
263                Ok(PropertyValue::Enumerated(self.credential_status))
264            }
265            p if p == PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS => Ok(PropertyValue::Unsigned(
266                self.assigned_access_rights_count as u64,
267            )),
268            p if p == PropertyIdentifier::AUTHENTICATION_FACTORS => Ok(PropertyValue::List(
269                self.authentication_factors
270                    .iter()
271                    .map(|f| PropertyValue::OctetString(f.clone()))
272                    .collect(),
273            )),
274            _ => Err(common::unknown_property_error()),
275        }
276    }
277
278    fn write_property(
279        &mut self,
280        property: PropertyIdentifier,
281        _array_index: Option<u32>,
282        value: PropertyValue,
283        _priority: Option<u8>,
284    ) -> Result<(), Error> {
285        if let Some(result) =
286            common::write_out_of_service(&mut self.out_of_service, property, &value)
287        {
288            return result;
289        }
290        if let Some(result) = common::write_description(&mut self.description, property, &value) {
291            return result;
292        }
293        match property {
294            p if p == PropertyIdentifier::PRESENT_VALUE => {
295                if let PropertyValue::Enumerated(v) = value {
296                    self.present_value = v;
297                    Ok(())
298                } else {
299                    Err(common::invalid_data_type_error())
300                }
301            }
302            p if p == PropertyIdentifier::CREDENTIAL_STATUS => {
303                if let PropertyValue::Enumerated(v) = value {
304                    self.credential_status = v;
305                    Ok(())
306                } else {
307                    Err(common::invalid_data_type_error())
308                }
309            }
310            _ => Err(common::write_access_denied_error()),
311        }
312    }
313
314    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
315        static PROPS: &[PropertyIdentifier] = &[
316            PropertyIdentifier::OBJECT_IDENTIFIER,
317            PropertyIdentifier::OBJECT_NAME,
318            PropertyIdentifier::DESCRIPTION,
319            PropertyIdentifier::OBJECT_TYPE,
320            PropertyIdentifier::PRESENT_VALUE,
321            PropertyIdentifier::CREDENTIAL_STATUS,
322            PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS,
323            PropertyIdentifier::AUTHENTICATION_FACTORS,
324            PropertyIdentifier::STATUS_FLAGS,
325            PropertyIdentifier::OUT_OF_SERVICE,
326            PropertyIdentifier::RELIABILITY,
327        ];
328        Cow::Borrowed(PROPS)
329    }
330}
331
332// ---------------------------------------------------------------------------
333// AccessPointObject (type 33)
334// ---------------------------------------------------------------------------
335
336/// BACnet Access Point object (type 33).
337///
338/// Represents an access point (reader/controller at a door) in an access control system.
339/// Present value indicates the most recent access event.
340pub struct AccessPointObject {
341    oid: ObjectIdentifier,
342    name: String,
343    description: String,
344    present_value: u32, // AccessEvent enumeration
345    access_event: u32,
346    access_event_tag: u64,
347    access_event_time: ([u8; 4], [u8; 4]), // (Date, Time) as raw bytes
348    access_doors: Vec<ObjectIdentifier>,
349    event_state: u32,
350    status_flags: StatusFlags,
351    out_of_service: bool,
352    reliability: u32,
353}
354
355impl AccessPointObject {
356    /// Create a new Access Point object.
357    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
358        let oid = ObjectIdentifier::new(ObjectType::ACCESS_POINT, instance)?;
359        Ok(Self {
360            oid,
361            name: name.into(),
362            description: String::new(),
363            present_value: 0,
364            access_event: 0,
365            access_event_tag: 0,
366            access_event_time: ([0xFF, 0xFF, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF, 0xFF]),
367            access_doors: Vec::new(),
368            event_state: 0,
369            status_flags: StatusFlags::empty(),
370            out_of_service: false,
371            reliability: 0,
372        })
373    }
374}
375
376impl BACnetObject for AccessPointObject {
377    fn object_identifier(&self) -> ObjectIdentifier {
378        self.oid
379    }
380
381    fn object_name(&self) -> &str {
382        &self.name
383    }
384
385    fn read_property(
386        &self,
387        property: PropertyIdentifier,
388        array_index: Option<u32>,
389    ) -> Result<PropertyValue, Error> {
390        if let Some(result) = read_common_properties!(self, property, array_index) {
391            return result;
392        }
393        match property {
394            p if p == PropertyIdentifier::OBJECT_TYPE => {
395                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_POINT.to_raw()))
396            }
397            p if p == PropertyIdentifier::PRESENT_VALUE => {
398                Ok(PropertyValue::Enumerated(self.present_value))
399            }
400            p if p == PropertyIdentifier::ACCESS_EVENT => {
401                Ok(PropertyValue::Enumerated(self.access_event))
402            }
403            p if p == PropertyIdentifier::ACCESS_EVENT_TAG => {
404                Ok(PropertyValue::Unsigned(self.access_event_tag))
405            }
406            p if p == PropertyIdentifier::ACCESS_EVENT_TIME => {
407                let (d, t) = &self.access_event_time;
408                Ok(PropertyValue::List(vec![
409                    PropertyValue::Date(Date {
410                        year: d[0],
411                        month: d[1],
412                        day: d[2],
413                        day_of_week: d[3],
414                    }),
415                    PropertyValue::Time(Time {
416                        hour: t[0],
417                        minute: t[1],
418                        second: t[2],
419                        hundredths: t[3],
420                    }),
421                ]))
422            }
423            p if p == PropertyIdentifier::ACCESS_DOORS => Ok(PropertyValue::List(
424                self.access_doors
425                    .iter()
426                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
427                    .collect(),
428            )),
429            p if p == PropertyIdentifier::EVENT_STATE => {
430                Ok(PropertyValue::Enumerated(self.event_state))
431            }
432            _ => Err(common::unknown_property_error()),
433        }
434    }
435
436    fn write_property(
437        &mut self,
438        property: PropertyIdentifier,
439        _array_index: Option<u32>,
440        value: PropertyValue,
441        _priority: Option<u8>,
442    ) -> Result<(), Error> {
443        if let Some(result) =
444            common::write_out_of_service(&mut self.out_of_service, property, &value)
445        {
446            return result;
447        }
448        if let Some(result) = common::write_description(&mut self.description, property, &value) {
449            return result;
450        }
451        match property {
452            p if p == PropertyIdentifier::PRESENT_VALUE => {
453                if let PropertyValue::Enumerated(v) = value {
454                    self.present_value = v;
455                    Ok(())
456                } else {
457                    Err(common::invalid_data_type_error())
458                }
459            }
460            _ => Err(common::write_access_denied_error()),
461        }
462    }
463
464    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
465        static PROPS: &[PropertyIdentifier] = &[
466            PropertyIdentifier::OBJECT_IDENTIFIER,
467            PropertyIdentifier::OBJECT_NAME,
468            PropertyIdentifier::DESCRIPTION,
469            PropertyIdentifier::OBJECT_TYPE,
470            PropertyIdentifier::PRESENT_VALUE,
471            PropertyIdentifier::ACCESS_EVENT,
472            PropertyIdentifier::ACCESS_EVENT_TAG,
473            PropertyIdentifier::ACCESS_EVENT_TIME,
474            PropertyIdentifier::ACCESS_DOORS,
475            PropertyIdentifier::EVENT_STATE,
476            PropertyIdentifier::STATUS_FLAGS,
477            PropertyIdentifier::OUT_OF_SERVICE,
478            PropertyIdentifier::RELIABILITY,
479        ];
480        Cow::Borrowed(PROPS)
481    }
482}
483
484// ---------------------------------------------------------------------------
485// AccessRightsObject (type 34)
486// ---------------------------------------------------------------------------
487
488/// BACnet Access Rights object (type 34).
489///
490/// Defines a set of access rules (positive and negative) that can be
491/// assigned to credentials and users.
492pub struct AccessRightsObject {
493    oid: ObjectIdentifier,
494    name: String,
495    description: String,
496    global_identifier: u64,
497    positive_access_rules_count: u32,
498    negative_access_rules_count: u32,
499    status_flags: StatusFlags,
500    out_of_service: bool,
501    reliability: u32,
502}
503
504impl AccessRightsObject {
505    /// Create a new Access Rights object.
506    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
507        let oid = ObjectIdentifier::new(ObjectType::ACCESS_RIGHTS, instance)?;
508        Ok(Self {
509            oid,
510            name: name.into(),
511            description: String::new(),
512            global_identifier: 0,
513            positive_access_rules_count: 0,
514            negative_access_rules_count: 0,
515            status_flags: StatusFlags::empty(),
516            out_of_service: false,
517            reliability: 0,
518        })
519    }
520}
521
522impl BACnetObject for AccessRightsObject {
523    fn object_identifier(&self) -> ObjectIdentifier {
524        self.oid
525    }
526
527    fn object_name(&self) -> &str {
528        &self.name
529    }
530
531    fn read_property(
532        &self,
533        property: PropertyIdentifier,
534        array_index: Option<u32>,
535    ) -> Result<PropertyValue, Error> {
536        if let Some(result) = read_common_properties!(self, property, array_index) {
537            return result;
538        }
539        match property {
540            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
541                ObjectType::ACCESS_RIGHTS.to_raw(),
542            )),
543            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
544                Ok(PropertyValue::Unsigned(self.global_identifier))
545            }
546            p if p == PropertyIdentifier::POSITIVE_ACCESS_RULES => Ok(PropertyValue::Unsigned(
547                self.positive_access_rules_count as u64,
548            )),
549            p if p == PropertyIdentifier::NEGATIVE_ACCESS_RULES => Ok(PropertyValue::Unsigned(
550                self.negative_access_rules_count as u64,
551            )),
552            _ => Err(common::unknown_property_error()),
553        }
554    }
555
556    fn write_property(
557        &mut self,
558        property: PropertyIdentifier,
559        _array_index: Option<u32>,
560        value: PropertyValue,
561        _priority: Option<u8>,
562    ) -> Result<(), Error> {
563        if let Some(result) =
564            common::write_out_of_service(&mut self.out_of_service, property, &value)
565        {
566            return result;
567        }
568        if let Some(result) = common::write_description(&mut self.description, property, &value) {
569            return result;
570        }
571        match property {
572            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
573                if let PropertyValue::Unsigned(v) = value {
574                    self.global_identifier = v;
575                    Ok(())
576                } else {
577                    Err(common::invalid_data_type_error())
578                }
579            }
580            _ => Err(common::write_access_denied_error()),
581        }
582    }
583
584    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
585        static PROPS: &[PropertyIdentifier] = &[
586            PropertyIdentifier::OBJECT_IDENTIFIER,
587            PropertyIdentifier::OBJECT_NAME,
588            PropertyIdentifier::DESCRIPTION,
589            PropertyIdentifier::OBJECT_TYPE,
590            PropertyIdentifier::GLOBAL_IDENTIFIER,
591            PropertyIdentifier::POSITIVE_ACCESS_RULES,
592            PropertyIdentifier::NEGATIVE_ACCESS_RULES,
593            PropertyIdentifier::STATUS_FLAGS,
594            PropertyIdentifier::OUT_OF_SERVICE,
595            PropertyIdentifier::RELIABILITY,
596        ];
597        Cow::Borrowed(PROPS)
598    }
599}
600
601// ---------------------------------------------------------------------------
602// AccessUserObject (type 35)
603// ---------------------------------------------------------------------------
604
605/// BACnet Access User object (type 35).
606///
607/// Represents a person or entity that uses credentials to gain access.
608/// Present value indicates the user type (AccessUserType enumeration).
609pub struct AccessUserObject {
610    oid: ObjectIdentifier,
611    name: String,
612    description: String,
613    present_value: u32, // AccessUserType enumeration
614    user_type: u32,
615    credentials: Vec<ObjectIdentifier>,
616    assigned_access_rights_count: u32,
617    status_flags: StatusFlags,
618    out_of_service: bool,
619    reliability: u32,
620}
621
622impl AccessUserObject {
623    /// Create a new Access User object.
624    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
625        let oid = ObjectIdentifier::new(ObjectType::ACCESS_USER, instance)?;
626        Ok(Self {
627            oid,
628            name: name.into(),
629            description: String::new(),
630            present_value: 0,
631            user_type: 0,
632            credentials: Vec::new(),
633            assigned_access_rights_count: 0,
634            status_flags: StatusFlags::empty(),
635            out_of_service: false,
636            reliability: 0,
637        })
638    }
639}
640
641impl BACnetObject for AccessUserObject {
642    fn object_identifier(&self) -> ObjectIdentifier {
643        self.oid
644    }
645
646    fn object_name(&self) -> &str {
647        &self.name
648    }
649
650    fn read_property(
651        &self,
652        property: PropertyIdentifier,
653        array_index: Option<u32>,
654    ) -> Result<PropertyValue, Error> {
655        if let Some(result) = read_common_properties!(self, property, array_index) {
656            return result;
657        }
658        match property {
659            p if p == PropertyIdentifier::OBJECT_TYPE => {
660                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_USER.to_raw()))
661            }
662            p if p == PropertyIdentifier::PRESENT_VALUE => {
663                Ok(PropertyValue::Enumerated(self.present_value))
664            }
665            p if p == PropertyIdentifier::USER_TYPE => {
666                Ok(PropertyValue::Enumerated(self.user_type))
667            }
668            p if p == PropertyIdentifier::CREDENTIALS => Ok(PropertyValue::List(
669                self.credentials
670                    .iter()
671                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
672                    .collect(),
673            )),
674            p if p == PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS => Ok(PropertyValue::Unsigned(
675                self.assigned_access_rights_count as u64,
676            )),
677            _ => Err(common::unknown_property_error()),
678        }
679    }
680
681    fn write_property(
682        &mut self,
683        property: PropertyIdentifier,
684        _array_index: Option<u32>,
685        value: PropertyValue,
686        _priority: Option<u8>,
687    ) -> Result<(), Error> {
688        if let Some(result) =
689            common::write_out_of_service(&mut self.out_of_service, property, &value)
690        {
691            return result;
692        }
693        if let Some(result) = common::write_description(&mut self.description, property, &value) {
694            return result;
695        }
696        match property {
697            p if p == PropertyIdentifier::PRESENT_VALUE => {
698                if let PropertyValue::Enumerated(v) = value {
699                    self.present_value = v;
700                    Ok(())
701                } else {
702                    Err(common::invalid_data_type_error())
703                }
704            }
705            p if p == PropertyIdentifier::USER_TYPE => {
706                if let PropertyValue::Enumerated(v) = value {
707                    self.user_type = v;
708                    Ok(())
709                } else {
710                    Err(common::invalid_data_type_error())
711                }
712            }
713            _ => Err(common::write_access_denied_error()),
714        }
715    }
716
717    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
718        static PROPS: &[PropertyIdentifier] = &[
719            PropertyIdentifier::OBJECT_IDENTIFIER,
720            PropertyIdentifier::OBJECT_NAME,
721            PropertyIdentifier::DESCRIPTION,
722            PropertyIdentifier::OBJECT_TYPE,
723            PropertyIdentifier::PRESENT_VALUE,
724            PropertyIdentifier::USER_TYPE,
725            PropertyIdentifier::CREDENTIALS,
726            PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS,
727            PropertyIdentifier::STATUS_FLAGS,
728            PropertyIdentifier::OUT_OF_SERVICE,
729            PropertyIdentifier::RELIABILITY,
730        ];
731        Cow::Borrowed(PROPS)
732    }
733}
734
735// ---------------------------------------------------------------------------
736// AccessZoneObject (type 36)
737// ---------------------------------------------------------------------------
738
739/// BACnet Access Zone object (type 36).
740///
741/// Represents a physical zone or area controlled by access points.
742/// Present value indicates the occupancy state (AccessZoneOccupancyState).
743pub struct AccessZoneObject {
744    oid: ObjectIdentifier,
745    name: String,
746    description: String,
747    present_value: u32, // AccessZoneOccupancyState enumeration
748    global_identifier: u64,
749    occupancy_count: u64,
750    access_doors: Vec<ObjectIdentifier>,
751    entry_points: Vec<ObjectIdentifier>,
752    exit_points: Vec<ObjectIdentifier>,
753    status_flags: StatusFlags,
754    out_of_service: bool,
755    reliability: u32,
756}
757
758impl AccessZoneObject {
759    /// Create a new Access Zone object.
760    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
761        let oid = ObjectIdentifier::new(ObjectType::ACCESS_ZONE, instance)?;
762        Ok(Self {
763            oid,
764            name: name.into(),
765            description: String::new(),
766            present_value: 0,
767            global_identifier: 0,
768            occupancy_count: 0,
769            access_doors: Vec::new(),
770            entry_points: Vec::new(),
771            exit_points: Vec::new(),
772            status_flags: StatusFlags::empty(),
773            out_of_service: false,
774            reliability: 0,
775        })
776    }
777}
778
779impl BACnetObject for AccessZoneObject {
780    fn object_identifier(&self) -> ObjectIdentifier {
781        self.oid
782    }
783
784    fn object_name(&self) -> &str {
785        &self.name
786    }
787
788    fn read_property(
789        &self,
790        property: PropertyIdentifier,
791        array_index: Option<u32>,
792    ) -> Result<PropertyValue, Error> {
793        if let Some(result) = read_common_properties!(self, property, array_index) {
794            return result;
795        }
796        match property {
797            p if p == PropertyIdentifier::OBJECT_TYPE => {
798                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_ZONE.to_raw()))
799            }
800            p if p == PropertyIdentifier::PRESENT_VALUE => {
801                Ok(PropertyValue::Enumerated(self.present_value))
802            }
803            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
804                Ok(PropertyValue::Unsigned(self.global_identifier))
805            }
806            p if p == PropertyIdentifier::OCCUPANCY_COUNT => {
807                Ok(PropertyValue::Unsigned(self.occupancy_count))
808            }
809            p if p == PropertyIdentifier::ACCESS_DOORS => Ok(PropertyValue::List(
810                self.access_doors
811                    .iter()
812                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
813                    .collect(),
814            )),
815            p if p == PropertyIdentifier::ENTRY_POINTS => Ok(PropertyValue::List(
816                self.entry_points
817                    .iter()
818                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
819                    .collect(),
820            )),
821            p if p == PropertyIdentifier::EXIT_POINTS => Ok(PropertyValue::List(
822                self.exit_points
823                    .iter()
824                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
825                    .collect(),
826            )),
827            _ => Err(common::unknown_property_error()),
828        }
829    }
830
831    fn write_property(
832        &mut self,
833        property: PropertyIdentifier,
834        _array_index: Option<u32>,
835        value: PropertyValue,
836        _priority: Option<u8>,
837    ) -> Result<(), Error> {
838        if let Some(result) =
839            common::write_out_of_service(&mut self.out_of_service, property, &value)
840        {
841            return result;
842        }
843        if let Some(result) = common::write_description(&mut self.description, property, &value) {
844            return result;
845        }
846        match property {
847            p if p == PropertyIdentifier::PRESENT_VALUE => {
848                if let PropertyValue::Enumerated(v) = value {
849                    self.present_value = v;
850                    Ok(())
851                } else {
852                    Err(common::invalid_data_type_error())
853                }
854            }
855            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
856                if let PropertyValue::Unsigned(v) = value {
857                    self.global_identifier = v;
858                    Ok(())
859                } else {
860                    Err(common::invalid_data_type_error())
861                }
862            }
863            _ => Err(common::write_access_denied_error()),
864        }
865    }
866
867    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
868        static PROPS: &[PropertyIdentifier] = &[
869            PropertyIdentifier::OBJECT_IDENTIFIER,
870            PropertyIdentifier::OBJECT_NAME,
871            PropertyIdentifier::DESCRIPTION,
872            PropertyIdentifier::OBJECT_TYPE,
873            PropertyIdentifier::PRESENT_VALUE,
874            PropertyIdentifier::GLOBAL_IDENTIFIER,
875            PropertyIdentifier::OCCUPANCY_COUNT,
876            PropertyIdentifier::ACCESS_DOORS,
877            PropertyIdentifier::ENTRY_POINTS,
878            PropertyIdentifier::EXIT_POINTS,
879            PropertyIdentifier::STATUS_FLAGS,
880            PropertyIdentifier::OUT_OF_SERVICE,
881            PropertyIdentifier::RELIABILITY,
882        ];
883        Cow::Borrowed(PROPS)
884    }
885}
886
887// ---------------------------------------------------------------------------
888// CredentialDataInputObject (type 37)
889// ---------------------------------------------------------------------------
890
891/// BACnet Credential Data Input object (type 37).
892///
893/// Represents a credential reader device (card reader, biometric scanner, etc.).
894/// Present value indicates the authentication status.
895pub struct CredentialDataInputObject {
896    oid: ObjectIdentifier,
897    name: String,
898    description: String,
899    present_value: u32,              // AuthenticationStatus: 0=notReady, 1=waiting
900    update_time: ([u8; 4], [u8; 4]), // (Date, Time) as raw bytes
901    supported_formats: Vec<u64>,
902    supported_format_classes: Vec<u64>,
903    status_flags: StatusFlags,
904    out_of_service: bool,
905    reliability: u32,
906}
907
908impl CredentialDataInputObject {
909    /// Create a new Credential Data Input object.
910    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
911        let oid = ObjectIdentifier::new(ObjectType::CREDENTIAL_DATA_INPUT, instance)?;
912        Ok(Self {
913            oid,
914            name: name.into(),
915            description: String::new(),
916            present_value: 0, // notReady
917            update_time: ([0xFF, 0xFF, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF, 0xFF]),
918            supported_formats: Vec::new(),
919            supported_format_classes: Vec::new(),
920            status_flags: StatusFlags::empty(),
921            out_of_service: false,
922            reliability: 0,
923        })
924    }
925}
926
927impl BACnetObject for CredentialDataInputObject {
928    fn object_identifier(&self) -> ObjectIdentifier {
929        self.oid
930    }
931
932    fn object_name(&self) -> &str {
933        &self.name
934    }
935
936    fn read_property(
937        &self,
938        property: PropertyIdentifier,
939        array_index: Option<u32>,
940    ) -> Result<PropertyValue, Error> {
941        if let Some(result) = read_common_properties!(self, property, array_index) {
942            return result;
943        }
944        match property {
945            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
946                ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
947            )),
948            p if p == PropertyIdentifier::PRESENT_VALUE => {
949                Ok(PropertyValue::Enumerated(self.present_value))
950            }
951            p if p == PropertyIdentifier::UPDATE_TIME => {
952                let (d, t) = &self.update_time;
953                Ok(PropertyValue::List(vec![
954                    PropertyValue::Date(Date {
955                        year: d[0],
956                        month: d[1],
957                        day: d[2],
958                        day_of_week: d[3],
959                    }),
960                    PropertyValue::Time(Time {
961                        hour: t[0],
962                        minute: t[1],
963                        second: t[2],
964                        hundredths: t[3],
965                    }),
966                ]))
967            }
968            p if p == PropertyIdentifier::SUPPORTED_FORMATS => Ok(PropertyValue::List(
969                self.supported_formats
970                    .iter()
971                    .map(|v| PropertyValue::Unsigned(*v))
972                    .collect(),
973            )),
974            p if p == PropertyIdentifier::SUPPORTED_FORMAT_CLASSES => Ok(PropertyValue::List(
975                self.supported_format_classes
976                    .iter()
977                    .map(|v| PropertyValue::Unsigned(*v))
978                    .collect(),
979            )),
980            _ => Err(common::unknown_property_error()),
981        }
982    }
983
984    fn write_property(
985        &mut self,
986        property: PropertyIdentifier,
987        _array_index: Option<u32>,
988        value: PropertyValue,
989        _priority: Option<u8>,
990    ) -> Result<(), Error> {
991        if let Some(result) =
992            common::write_out_of_service(&mut self.out_of_service, property, &value)
993        {
994            return result;
995        }
996        if let Some(result) = common::write_description(&mut self.description, property, &value) {
997            return result;
998        }
999        // CredentialDataInput is primarily read-only (driven by hardware)
1000        Err(common::write_access_denied_error())
1001    }
1002
1003    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
1004        static PROPS: &[PropertyIdentifier] = &[
1005            PropertyIdentifier::OBJECT_IDENTIFIER,
1006            PropertyIdentifier::OBJECT_NAME,
1007            PropertyIdentifier::DESCRIPTION,
1008            PropertyIdentifier::OBJECT_TYPE,
1009            PropertyIdentifier::PRESENT_VALUE,
1010            PropertyIdentifier::UPDATE_TIME,
1011            PropertyIdentifier::SUPPORTED_FORMATS,
1012            PropertyIdentifier::SUPPORTED_FORMAT_CLASSES,
1013            PropertyIdentifier::STATUS_FLAGS,
1014            PropertyIdentifier::OUT_OF_SERVICE,
1015            PropertyIdentifier::RELIABILITY,
1016        ];
1017        Cow::Borrowed(PROPS)
1018    }
1019}
1020
1021// ---------------------------------------------------------------------------
1022// Tests
1023// ---------------------------------------------------------------------------
1024
1025#[cfg(test)]
1026mod tests {
1027    use super::*;
1028
1029    // --- AccessDoorObject ---
1030
1031    #[test]
1032    fn access_door_create_and_read_defaults() {
1033        let door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1034        assert_eq!(door.object_name(), "DOOR-1");
1035        assert_eq!(
1036            door.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1037                .unwrap(),
1038            PropertyValue::Enumerated(0) // closed
1039        );
1040    }
1041
1042    #[test]
1043    fn access_door_object_type() {
1044        let door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1045        assert_eq!(
1046            door.read_property(PropertyIdentifier::OBJECT_TYPE, None)
1047                .unwrap(),
1048            PropertyValue::Enumerated(ObjectType::ACCESS_DOOR.to_raw())
1049        );
1050    }
1051
1052    #[test]
1053    fn access_door_property_list() {
1054        let door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1055        let list = door.property_list();
1056        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
1057        assert!(list.contains(&PropertyIdentifier::DOOR_STATUS));
1058        assert!(list.contains(&PropertyIdentifier::LOCK_STATUS));
1059        assert!(list.contains(&PropertyIdentifier::SECURED_STATUS));
1060        assert!(list.contains(&PropertyIdentifier::DOOR_ALARM_STATE));
1061        assert!(list.contains(&PropertyIdentifier::DOOR_MEMBERS));
1062    }
1063
1064    #[test]
1065    fn access_door_read_door_members_empty() {
1066        let door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1067        assert_eq!(
1068            door.read_property(PropertyIdentifier::DOOR_MEMBERS, None)
1069                .unwrap(),
1070            PropertyValue::List(vec![])
1071        );
1072    }
1073
1074    #[test]
1075    fn access_door_write_present_value() {
1076        let mut door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1077        // Must be out-of-service to write present value
1078        door.write_property(
1079            PropertyIdentifier::OUT_OF_SERVICE,
1080            None,
1081            PropertyValue::Boolean(true),
1082            None,
1083        )
1084        .unwrap();
1085        door.write_property(
1086            PropertyIdentifier::PRESENT_VALUE,
1087            None,
1088            PropertyValue::Enumerated(1), // opened
1089            None,
1090        )
1091        .unwrap();
1092        assert_eq!(
1093            door.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1094                .unwrap(),
1095            PropertyValue::Enumerated(1)
1096        );
1097    }
1098
1099    #[test]
1100    fn access_door_write_present_value_commandable() {
1101        let mut door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1102        // AccessDoor is commandable — writing PV with priority should succeed
1103        let result = door.write_property(
1104            PropertyIdentifier::PRESENT_VALUE,
1105            None,
1106            PropertyValue::Enumerated(1), // opened
1107            Some(16),
1108        );
1109        assert!(result.is_ok());
1110        // Verify PV changed
1111        let pv = door
1112            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
1113            .unwrap();
1114        assert_eq!(pv, PropertyValue::Enumerated(1));
1115        // Relinquish — write NULL
1116        let result = door.write_property(
1117            PropertyIdentifier::PRESENT_VALUE,
1118            None,
1119            PropertyValue::Null,
1120            Some(16),
1121        );
1122        assert!(result.is_ok());
1123        // PV should revert to relinquish default (0 = closed)
1124        let pv = door
1125            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
1126            .unwrap();
1127        assert_eq!(pv, PropertyValue::Enumerated(0));
1128    }
1129
1130    #[test]
1131    fn access_door_write_present_value_wrong_type() {
1132        let mut door = AccessDoorObject::new(1, "DOOR-1").unwrap();
1133        door.write_property(
1134            PropertyIdentifier::OUT_OF_SERVICE,
1135            None,
1136            PropertyValue::Boolean(true),
1137            None,
1138        )
1139        .unwrap();
1140        let result = door.write_property(
1141            PropertyIdentifier::PRESENT_VALUE,
1142            None,
1143            PropertyValue::Real(1.0),
1144            None,
1145        );
1146        assert!(result.is_err());
1147    }
1148
1149    // --- AccessCredentialObject ---
1150
1151    #[test]
1152    fn access_credential_create_and_read_defaults() {
1153        let cred = AccessCredentialObject::new(1, "CRED-1").unwrap();
1154        assert_eq!(cred.object_name(), "CRED-1");
1155        assert_eq!(
1156            cred.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1157                .unwrap(),
1158            PropertyValue::Enumerated(0) // inactive
1159        );
1160    }
1161
1162    #[test]
1163    fn access_credential_object_type() {
1164        let cred = AccessCredentialObject::new(1, "CRED-1").unwrap();
1165        assert_eq!(
1166            cred.read_property(PropertyIdentifier::OBJECT_TYPE, None)
1167                .unwrap(),
1168            PropertyValue::Enumerated(ObjectType::ACCESS_CREDENTIAL.to_raw())
1169        );
1170    }
1171
1172    #[test]
1173    fn access_credential_property_list() {
1174        let cred = AccessCredentialObject::new(1, "CRED-1").unwrap();
1175        let list = cred.property_list();
1176        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
1177        assert!(list.contains(&PropertyIdentifier::CREDENTIAL_STATUS));
1178        assert!(list.contains(&PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS));
1179        assert!(list.contains(&PropertyIdentifier::AUTHENTICATION_FACTORS));
1180    }
1181
1182    #[test]
1183    fn access_credential_read_assigned_access_rights() {
1184        let cred = AccessCredentialObject::new(1, "CRED-1").unwrap();
1185        assert_eq!(
1186            cred.read_property(PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS, None)
1187                .unwrap(),
1188            PropertyValue::Unsigned(0)
1189        );
1190    }
1191
1192    #[test]
1193    fn access_credential_read_authentication_factors() {
1194        let cred = AccessCredentialObject::new(1, "CRED-1").unwrap();
1195        assert_eq!(
1196            cred.read_property(PropertyIdentifier::AUTHENTICATION_FACTORS, None)
1197                .unwrap(),
1198            PropertyValue::List(vec![])
1199        );
1200    }
1201
1202    // --- AccessPointObject ---
1203
1204    #[test]
1205    fn access_point_create_and_read_defaults() {
1206        let point = AccessPointObject::new(1, "AP-1").unwrap();
1207        assert_eq!(point.object_name(), "AP-1");
1208        assert_eq!(
1209            point
1210                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
1211                .unwrap(),
1212            PropertyValue::Enumerated(0)
1213        );
1214    }
1215
1216    #[test]
1217    fn access_point_object_type() {
1218        let point = AccessPointObject::new(1, "AP-1").unwrap();
1219        assert_eq!(
1220            point
1221                .read_property(PropertyIdentifier::OBJECT_TYPE, None)
1222                .unwrap(),
1223            PropertyValue::Enumerated(ObjectType::ACCESS_POINT.to_raw())
1224        );
1225    }
1226
1227    #[test]
1228    fn access_point_property_list() {
1229        let point = AccessPointObject::new(1, "AP-1").unwrap();
1230        let list = point.property_list();
1231        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
1232        assert!(list.contains(&PropertyIdentifier::ACCESS_EVENT));
1233        assert!(list.contains(&PropertyIdentifier::ACCESS_EVENT_TAG));
1234        assert!(list.contains(&PropertyIdentifier::ACCESS_EVENT_TIME));
1235        assert!(list.contains(&PropertyIdentifier::ACCESS_DOORS));
1236        assert!(list.contains(&PropertyIdentifier::EVENT_STATE));
1237    }
1238
1239    #[test]
1240    fn access_point_read_access_event_time() {
1241        let point = AccessPointObject::new(1, "AP-1").unwrap();
1242        let val = point
1243            .read_property(PropertyIdentifier::ACCESS_EVENT_TIME, None)
1244            .unwrap();
1245        match val {
1246            PropertyValue::List(items) => {
1247                assert_eq!(items.len(), 2);
1248            }
1249            other => panic!("expected List, got {other:?}"),
1250        }
1251    }
1252
1253    #[test]
1254    fn access_point_read_access_doors_empty() {
1255        let point = AccessPointObject::new(1, "AP-1").unwrap();
1256        assert_eq!(
1257            point
1258                .read_property(PropertyIdentifier::ACCESS_DOORS, None)
1259                .unwrap(),
1260            PropertyValue::List(vec![])
1261        );
1262    }
1263
1264    // --- AccessRightsObject ---
1265
1266    #[test]
1267    fn access_rights_create_and_read_defaults() {
1268        let rights = AccessRightsObject::new(1, "AR-1").unwrap();
1269        assert_eq!(rights.object_name(), "AR-1");
1270        assert_eq!(
1271            rights
1272                .read_property(PropertyIdentifier::GLOBAL_IDENTIFIER, None)
1273                .unwrap(),
1274            PropertyValue::Unsigned(0)
1275        );
1276    }
1277
1278    #[test]
1279    fn access_rights_object_type() {
1280        let rights = AccessRightsObject::new(1, "AR-1").unwrap();
1281        assert_eq!(
1282            rights
1283                .read_property(PropertyIdentifier::OBJECT_TYPE, None)
1284                .unwrap(),
1285            PropertyValue::Enumerated(ObjectType::ACCESS_RIGHTS.to_raw())
1286        );
1287    }
1288
1289    #[test]
1290    fn access_rights_property_list() {
1291        let rights = AccessRightsObject::new(1, "AR-1").unwrap();
1292        let list = rights.property_list();
1293        assert!(list.contains(&PropertyIdentifier::GLOBAL_IDENTIFIER));
1294        assert!(list.contains(&PropertyIdentifier::POSITIVE_ACCESS_RULES));
1295        assert!(list.contains(&PropertyIdentifier::NEGATIVE_ACCESS_RULES));
1296    }
1297
1298    #[test]
1299    fn access_rights_read_rules_counts() {
1300        let rights = AccessRightsObject::new(1, "AR-1").unwrap();
1301        assert_eq!(
1302            rights
1303                .read_property(PropertyIdentifier::POSITIVE_ACCESS_RULES, None)
1304                .unwrap(),
1305            PropertyValue::Unsigned(0)
1306        );
1307        assert_eq!(
1308            rights
1309                .read_property(PropertyIdentifier::NEGATIVE_ACCESS_RULES, None)
1310                .unwrap(),
1311            PropertyValue::Unsigned(0)
1312        );
1313    }
1314
1315    #[test]
1316    fn access_rights_write_global_identifier() {
1317        let mut rights = AccessRightsObject::new(1, "AR-1").unwrap();
1318        rights
1319            .write_property(
1320                PropertyIdentifier::GLOBAL_IDENTIFIER,
1321                None,
1322                PropertyValue::Unsigned(42),
1323                None,
1324            )
1325            .unwrap();
1326        assert_eq!(
1327            rights
1328                .read_property(PropertyIdentifier::GLOBAL_IDENTIFIER, None)
1329                .unwrap(),
1330            PropertyValue::Unsigned(42)
1331        );
1332    }
1333
1334    // --- AccessUserObject ---
1335
1336    #[test]
1337    fn access_user_create_and_read_defaults() {
1338        let user = AccessUserObject::new(1, "USER-1").unwrap();
1339        assert_eq!(user.object_name(), "USER-1");
1340        assert_eq!(
1341            user.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1342                .unwrap(),
1343            PropertyValue::Enumerated(0)
1344        );
1345    }
1346
1347    #[test]
1348    fn access_user_object_type() {
1349        let user = AccessUserObject::new(1, "USER-1").unwrap();
1350        assert_eq!(
1351            user.read_property(PropertyIdentifier::OBJECT_TYPE, None)
1352                .unwrap(),
1353            PropertyValue::Enumerated(ObjectType::ACCESS_USER.to_raw())
1354        );
1355    }
1356
1357    #[test]
1358    fn access_user_property_list() {
1359        let user = AccessUserObject::new(1, "USER-1").unwrap();
1360        let list = user.property_list();
1361        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
1362        assert!(list.contains(&PropertyIdentifier::USER_TYPE));
1363        assert!(list.contains(&PropertyIdentifier::CREDENTIALS));
1364        assert!(list.contains(&PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS));
1365    }
1366
1367    #[test]
1368    fn access_user_read_credentials_empty() {
1369        let user = AccessUserObject::new(1, "USER-1").unwrap();
1370        assert_eq!(
1371            user.read_property(PropertyIdentifier::CREDENTIALS, None)
1372                .unwrap(),
1373            PropertyValue::List(vec![])
1374        );
1375    }
1376
1377    #[test]
1378    fn access_user_write_user_type() {
1379        let mut user = AccessUserObject::new(1, "USER-1").unwrap();
1380        user.write_property(
1381            PropertyIdentifier::USER_TYPE,
1382            None,
1383            PropertyValue::Enumerated(1),
1384            None,
1385        )
1386        .unwrap();
1387        assert_eq!(
1388            user.read_property(PropertyIdentifier::USER_TYPE, None)
1389                .unwrap(),
1390            PropertyValue::Enumerated(1)
1391        );
1392    }
1393
1394    // --- AccessZoneObject ---
1395
1396    #[test]
1397    fn access_zone_create_and_read_defaults() {
1398        let zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
1399        assert_eq!(zone.object_name(), "ZONE-1");
1400        assert_eq!(
1401            zone.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1402                .unwrap(),
1403            PropertyValue::Enumerated(0)
1404        );
1405    }
1406
1407    #[test]
1408    fn access_zone_object_type() {
1409        let zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
1410        assert_eq!(
1411            zone.read_property(PropertyIdentifier::OBJECT_TYPE, None)
1412                .unwrap(),
1413            PropertyValue::Enumerated(ObjectType::ACCESS_ZONE.to_raw())
1414        );
1415    }
1416
1417    #[test]
1418    fn access_zone_property_list() {
1419        let zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
1420        let list = zone.property_list();
1421        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
1422        assert!(list.contains(&PropertyIdentifier::GLOBAL_IDENTIFIER));
1423        assert!(list.contains(&PropertyIdentifier::OCCUPANCY_COUNT));
1424        assert!(list.contains(&PropertyIdentifier::ACCESS_DOORS));
1425        assert!(list.contains(&PropertyIdentifier::ENTRY_POINTS));
1426        assert!(list.contains(&PropertyIdentifier::EXIT_POINTS));
1427    }
1428
1429    #[test]
1430    fn access_zone_read_lists_empty() {
1431        let zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
1432        assert_eq!(
1433            zone.read_property(PropertyIdentifier::ACCESS_DOORS, None)
1434                .unwrap(),
1435            PropertyValue::List(vec![])
1436        );
1437        assert_eq!(
1438            zone.read_property(PropertyIdentifier::ENTRY_POINTS, None)
1439                .unwrap(),
1440            PropertyValue::List(vec![])
1441        );
1442        assert_eq!(
1443            zone.read_property(PropertyIdentifier::EXIT_POINTS, None)
1444                .unwrap(),
1445            PropertyValue::List(vec![])
1446        );
1447    }
1448
1449    #[test]
1450    fn access_zone_read_occupancy_count() {
1451        let zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
1452        assert_eq!(
1453            zone.read_property(PropertyIdentifier::OCCUPANCY_COUNT, None)
1454                .unwrap(),
1455            PropertyValue::Unsigned(0)
1456        );
1457    }
1458
1459    #[test]
1460    fn access_zone_write_global_identifier() {
1461        let mut zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
1462        zone.write_property(
1463            PropertyIdentifier::GLOBAL_IDENTIFIER,
1464            None,
1465            PropertyValue::Unsigned(99),
1466            None,
1467        )
1468        .unwrap();
1469        assert_eq!(
1470            zone.read_property(PropertyIdentifier::GLOBAL_IDENTIFIER, None)
1471                .unwrap(),
1472            PropertyValue::Unsigned(99)
1473        );
1474    }
1475
1476    // --- CredentialDataInputObject ---
1477
1478    #[test]
1479    fn credential_data_input_create_and_read_defaults() {
1480        let cdi = CredentialDataInputObject::new(1, "CDI-1").unwrap();
1481        assert_eq!(cdi.object_name(), "CDI-1");
1482        assert_eq!(
1483            cdi.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1484                .unwrap(),
1485            PropertyValue::Enumerated(0) // notReady
1486        );
1487    }
1488
1489    #[test]
1490    fn credential_data_input_object_type() {
1491        let cdi = CredentialDataInputObject::new(1, "CDI-1").unwrap();
1492        assert_eq!(
1493            cdi.read_property(PropertyIdentifier::OBJECT_TYPE, None)
1494                .unwrap(),
1495            PropertyValue::Enumerated(ObjectType::CREDENTIAL_DATA_INPUT.to_raw())
1496        );
1497    }
1498
1499    #[test]
1500    fn credential_data_input_property_list() {
1501        let cdi = CredentialDataInputObject::new(1, "CDI-1").unwrap();
1502        let list = cdi.property_list();
1503        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
1504        assert!(list.contains(&PropertyIdentifier::UPDATE_TIME));
1505        assert!(list.contains(&PropertyIdentifier::SUPPORTED_FORMATS));
1506        assert!(list.contains(&PropertyIdentifier::SUPPORTED_FORMAT_CLASSES));
1507    }
1508
1509    #[test]
1510    fn credential_data_input_read_update_time() {
1511        let cdi = CredentialDataInputObject::new(1, "CDI-1").unwrap();
1512        let val = cdi
1513            .read_property(PropertyIdentifier::UPDATE_TIME, None)
1514            .unwrap();
1515        match val {
1516            PropertyValue::List(items) => {
1517                assert_eq!(items.len(), 2);
1518            }
1519            other => panic!("expected List, got {other:?}"),
1520        }
1521    }
1522
1523    #[test]
1524    fn credential_data_input_read_supported_formats_empty() {
1525        let cdi = CredentialDataInputObject::new(1, "CDI-1").unwrap();
1526        assert_eq!(
1527            cdi.read_property(PropertyIdentifier::SUPPORTED_FORMATS, None)
1528                .unwrap(),
1529            PropertyValue::List(vec![])
1530        );
1531    }
1532
1533    #[test]
1534    fn credential_data_input_write_denied() {
1535        let mut cdi = CredentialDataInputObject::new(1, "CDI-1").unwrap();
1536        let result = cdi.write_property(
1537            PropertyIdentifier::PRESENT_VALUE,
1538            None,
1539            PropertyValue::Enumerated(1),
1540            None,
1541        );
1542        assert!(result.is_err());
1543    }
1544}