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