Skip to main content

bacnet_objects/
device.rs

1//! Device object (type 8) per ASHRAE 135-2020 Clause 12.11.
2//!
3//! The Device object is required in every BACnet device and exposes
4//! device-level properties such as vendor info, protocol support,
5//! and configuration parameters.
6
7use std::borrow::Cow;
8use std::collections::HashMap;
9
10use bacnet_types::constructed::BACnetCOVSubscription;
11use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Segmentation};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, Time};
14
15use crate::common::read_property_list_property;
16use crate::traits::BACnetObject;
17
18/// Build a BACnet bitstring representing supported object types.
19/// Each type N sets bit at byte N/8, position 7-(N%8) (MSB-first within each byte).
20fn compute_object_types_supported(types: &[u32]) -> Vec<u8> {
21    let max_type = types.iter().copied().max().unwrap_or(0) as usize;
22    let num_bytes = (max_type / 8) + 1;
23    let mut bitstring = vec![0u8; num_bytes];
24    for &t in types {
25        let byte_idx = (t as usize) / 8;
26        let bit_pos = 7 - ((t as usize) % 8);
27        if byte_idx < bitstring.len() {
28            bitstring[byte_idx] |= 1 << bit_pos;
29        }
30    }
31    bitstring
32}
33
34/// Configuration for creating a Device object.
35pub struct DeviceConfig {
36    /// Device instance number (0..4194303).
37    pub instance: u32,
38    /// Device object name.
39    pub name: String,
40    /// Vendor name string.
41    pub vendor_name: String,
42    /// ASHRAE-assigned vendor identifier.
43    pub vendor_id: u16,
44    /// Model name string.
45    pub model_name: String,
46    /// Firmware revision string.
47    pub firmware_revision: String,
48    /// Application software version string.
49    pub application_software_version: String,
50    /// Maximum APDU length accepted (typically 1476 for BIP).
51    pub max_apdu_length: u32,
52    /// Segmentation support level.
53    pub segmentation_supported: Segmentation,
54    /// APDU timeout in milliseconds.
55    pub apdu_timeout: u32,
56    /// Number of APDU retries.
57    pub apdu_retries: u32,
58}
59
60impl Default for DeviceConfig {
61    fn default() -> Self {
62        Self {
63            instance: 1,
64            name: "BACnet Device".into(),
65            vendor_name: "Rusty BACnet".into(),
66            vendor_id: 0,
67            model_name: "rusty-bacnet".into(),
68            firmware_revision: "0.1.0".into(),
69            application_software_version: "0.1.0".into(),
70            max_apdu_length: 1476,
71            segmentation_supported: Segmentation::NONE,
72            apdu_timeout: 6000,
73            apdu_retries: 3,
74        }
75    }
76}
77
78/// BACnet Device object.
79pub struct DeviceObject {
80    oid: ObjectIdentifier,
81    properties: HashMap<PropertyIdentifier, PropertyValue>,
82    /// Cached object list for array-indexed reads.
83    object_list: Vec<ObjectIdentifier>,
84    /// Protocol_Object_Types_Supported — bitstring indicating which object
85    /// types this device supports (one bit per type, MSB-first within each byte).
86    protocol_object_types_supported: Vec<u8>,
87    /// Protocol_Services_Supported — bitstring indicating which services
88    /// this device supports (one bit per service, MSB-first within each byte).
89    protocol_services_supported: Vec<u8>,
90    /// Active COV subscriptions maintained by the server.
91    active_cov_subscriptions: Vec<BACnetCOVSubscription>,
92}
93
94impl DeviceObject {
95    /// Create a new Device object from configuration.
96    pub fn new(config: DeviceConfig) -> Result<Self, Error> {
97        let oid = ObjectIdentifier::new(ObjectType::DEVICE, config.instance)?;
98        let mut properties = HashMap::new();
99
100        properties.insert(
101            PropertyIdentifier::OBJECT_IDENTIFIER,
102            PropertyValue::ObjectIdentifier(oid),
103        );
104        properties.insert(
105            PropertyIdentifier::OBJECT_NAME,
106            PropertyValue::CharacterString(config.name),
107        );
108        properties.insert(
109            PropertyIdentifier::OBJECT_TYPE,
110            PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()),
111        );
112        properties.insert(
113            PropertyIdentifier::SYSTEM_STATUS,
114            PropertyValue::Enumerated(0), // operational
115        );
116        properties.insert(
117            PropertyIdentifier::VENDOR_NAME,
118            PropertyValue::CharacterString(config.vendor_name),
119        );
120        properties.insert(
121            PropertyIdentifier::VENDOR_IDENTIFIER,
122            PropertyValue::Unsigned(config.vendor_id as u64),
123        );
124        properties.insert(
125            PropertyIdentifier::MODEL_NAME,
126            PropertyValue::CharacterString(config.model_name),
127        );
128        properties.insert(
129            PropertyIdentifier::FIRMWARE_REVISION,
130            PropertyValue::CharacterString(config.firmware_revision),
131        );
132        properties.insert(
133            PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
134            PropertyValue::CharacterString(config.application_software_version),
135        );
136        properties.insert(
137            PropertyIdentifier::PROTOCOL_VERSION,
138            PropertyValue::Unsigned(1),
139        );
140        properties.insert(
141            PropertyIdentifier::PROTOCOL_REVISION,
142            PropertyValue::Unsigned(22), // Revision 22 (2020)
143        );
144        properties.insert(
145            PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
146            PropertyValue::Unsigned(config.max_apdu_length as u64),
147        );
148        properties.insert(
149            PropertyIdentifier::SEGMENTATION_SUPPORTED,
150            PropertyValue::Enumerated(config.segmentation_supported.to_raw() as u32),
151        );
152        properties.insert(
153            PropertyIdentifier::APDU_TIMEOUT,
154            PropertyValue::Unsigned(config.apdu_timeout as u64),
155        );
156        properties.insert(
157            PropertyIdentifier::NUMBER_OF_APDU_RETRIES,
158            PropertyValue::Unsigned(config.apdu_retries as u64),
159        );
160        properties.insert(
161            PropertyIdentifier::DATABASE_REVISION,
162            PropertyValue::Unsigned(0),
163        );
164        properties.insert(
165            PropertyIdentifier::DESCRIPTION,
166            PropertyValue::CharacterString(String::new()),
167        );
168
169        // Device_Address_Binding — starts empty; populated as devices are discovered.
170        properties.insert(
171            PropertyIdentifier::DEVICE_ADDRESS_BINDING,
172            PropertyValue::List(Vec::new()),
173        );
174
175        // Placeholder values updated by the server's time sync or system clock.
176        properties.insert(
177            PropertyIdentifier::LOCAL_DATE,
178            PropertyValue::Date(Date {
179                year: 126, // 2026 - 1900
180                month: 3,
181                day: 18,
182                day_of_week: 3, // Wednesday
183            }),
184        );
185        properties.insert(
186            PropertyIdentifier::LOCAL_TIME,
187            PropertyValue::Time(Time {
188                hour: 12,
189                minute: 0,
190                second: 0,
191                hundredths: 0,
192            }),
193        );
194
195        // UTC_Offset: signed integer minutes from UTC (e.g., -300 for EST).
196        properties.insert(
197            PropertyIdentifier::UTC_OFFSET,
198            PropertyValue::Signed(0), // UTC
199        );
200
201        // Last_Restart_Reason: 0=unknown, 1=coldstart, 2=warmstart, etc.
202        properties.insert(
203            PropertyIdentifier::LAST_RESTART_REASON,
204            PropertyValue::Enumerated(0), // unknown
205        );
206
207        // Device_UUID: 16-byte UUID stored as OctetString. Default: all zeros.
208        properties.insert(
209            PropertyIdentifier::DEVICE_UUID,
210            PropertyValue::OctetString(vec![0u8; 16]),
211        );
212
213        // Max_Segments_Accepted — only included when segmentation is supported.
214        if config.segmentation_supported != Segmentation::NONE {
215            properties.insert(
216                PropertyIdentifier::MAX_SEGMENTS_ACCEPTED,
217                PropertyValue::Unsigned(65), // default: more than 64 segments
218            );
219        }
220
221        // Protocol_Object_Types_Supported: bitstring with one bit per
222        // implemented object type.  Computed from the full set of types
223        // that have concrete struct implementations in this crate.
224        let protocol_object_types_supported = compute_object_types_supported(&[
225            ObjectType::ANALOG_INPUT.to_raw(),
226            ObjectType::ANALOG_OUTPUT.to_raw(),
227            ObjectType::ANALOG_VALUE.to_raw(),
228            ObjectType::BINARY_INPUT.to_raw(),
229            ObjectType::BINARY_OUTPUT.to_raw(),
230            ObjectType::BINARY_VALUE.to_raw(),
231            ObjectType::CALENDAR.to_raw(),
232            ObjectType::COMMAND.to_raw(),
233            ObjectType::DEVICE.to_raw(),
234            ObjectType::EVENT_ENROLLMENT.to_raw(),
235            ObjectType::FILE.to_raw(),
236            ObjectType::GROUP.to_raw(),
237            ObjectType::LOOP.to_raw(),
238            ObjectType::MULTI_STATE_INPUT.to_raw(),
239            ObjectType::MULTI_STATE_OUTPUT.to_raw(),
240            ObjectType::NOTIFICATION_CLASS.to_raw(),
241            ObjectType::PROGRAM.to_raw(),
242            ObjectType::SCHEDULE.to_raw(),
243            ObjectType::AVERAGING.to_raw(),
244            ObjectType::MULTI_STATE_VALUE.to_raw(),
245            ObjectType::TREND_LOG.to_raw(),
246            ObjectType::LIFE_SAFETY_POINT.to_raw(),
247            ObjectType::LIFE_SAFETY_ZONE.to_raw(),
248            ObjectType::ACCUMULATOR.to_raw(),
249            ObjectType::PULSE_CONVERTER.to_raw(),
250            ObjectType::EVENT_LOG.to_raw(),
251            ObjectType::GLOBAL_GROUP.to_raw(),
252            ObjectType::TREND_LOG_MULTIPLE.to_raw(),
253            ObjectType::LOAD_CONTROL.to_raw(),
254            ObjectType::STRUCTURED_VIEW.to_raw(),
255            ObjectType::ACCESS_DOOR.to_raw(),
256            ObjectType::TIMER.to_raw(),
257            ObjectType::ACCESS_CREDENTIAL.to_raw(),
258            ObjectType::ACCESS_POINT.to_raw(),
259            ObjectType::ACCESS_RIGHTS.to_raw(),
260            ObjectType::ACCESS_USER.to_raw(),
261            ObjectType::ACCESS_ZONE.to_raw(),
262            ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
263            ObjectType::BITSTRING_VALUE.to_raw(),
264            ObjectType::CHARACTERSTRING_VALUE.to_raw(),
265            ObjectType::DATEPATTERN_VALUE.to_raw(),
266            ObjectType::DATE_VALUE.to_raw(),
267            ObjectType::DATETIMEPATTERN_VALUE.to_raw(),
268            ObjectType::DATETIME_VALUE.to_raw(),
269            ObjectType::INTEGER_VALUE.to_raw(),
270            ObjectType::LARGE_ANALOG_VALUE.to_raw(),
271            ObjectType::OCTETSTRING_VALUE.to_raw(),
272            ObjectType::POSITIVE_INTEGER_VALUE.to_raw(),
273            ObjectType::TIMEPATTERN_VALUE.to_raw(),
274            ObjectType::TIME_VALUE.to_raw(),
275            ObjectType::NOTIFICATION_FORWARDER.to_raw(),
276            ObjectType::ALERT_ENROLLMENT.to_raw(),
277            ObjectType::CHANNEL.to_raw(),
278            ObjectType::LIGHTING_OUTPUT.to_raw(),
279            ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
280            ObjectType::NETWORK_PORT.to_raw(),
281            ObjectType::ELEVATOR_GROUP.to_raw(),
282            ObjectType::ESCALATOR.to_raw(),
283            ObjectType::LIFT.to_raw(),
284            ObjectType::STAGING.to_raw(),
285            ObjectType::AUDIT_REPORTER.to_raw(),
286            ObjectType::AUDIT_LOG.to_raw(),
287            ObjectType::COLOR.to_raw(),
288            ObjectType::COLOR_TEMPERATURE.to_raw(),
289        ]);
290
291        // Protocol_Services_Supported: 6 bytes (48 bits).  Bits set for
292        // services we handle:
293        //   0=AcknowledgeAlarm, 2=ConfirmedEventNotification,
294        //   5=SubscribeCOV, 12=ReadProperty, 14=ReadPropertyMultiple,
295        //   15=WriteProperty, 16=WritePropertyMultiple,
296        //   26=IAm, 27=IHave, 29=UnconfirmedCOVNotification,
297        //   31=WhoHas, 32=WhoIs
298        //   Byte 0: bits 0,2,5 → 0xA4
299        //   Byte 1: bits 12,14,15 → 0x0B
300        //   Byte 2: bit 16 → 0x80
301        //   Byte 3: bits 26,27,29,31 → 0x35
302        //   Byte 4: bit 32 → 0x80
303        //   Byte 5: 0x00
304        let protocol_services_supported = vec![0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00];
305
306        Ok(Self {
307            oid,
308            properties,
309            object_list: vec![oid], // Device itself is always in the list
310            protocol_object_types_supported,
311            protocol_services_supported,
312            active_cov_subscriptions: Vec::new(),
313        })
314    }
315
316    /// Update the object-list with the current database contents.
317    pub fn set_object_list(&mut self, oids: Vec<ObjectIdentifier>) {
318        self.object_list = oids;
319    }
320
321    /// Get the device instance number.
322    pub fn instance(&self) -> u32 {
323        self.oid.instance_number()
324    }
325
326    /// Set the description string.
327    pub fn set_description(&mut self, desc: impl Into<String>) {
328        self.properties.insert(
329            PropertyIdentifier::DESCRIPTION,
330            PropertyValue::CharacterString(desc.into()),
331        );
332    }
333
334    /// Replace the entire active COV subscriptions list.
335    pub fn set_active_cov_subscriptions(&mut self, subs: Vec<BACnetCOVSubscription>) {
336        self.active_cov_subscriptions = subs;
337    }
338
339    /// Add a single COV subscription.
340    pub fn add_cov_subscription(&mut self, sub: BACnetCOVSubscription) {
341        self.active_cov_subscriptions.push(sub);
342    }
343}
344
345impl BACnetObject for DeviceObject {
346    fn object_identifier(&self) -> ObjectIdentifier {
347        self.oid
348    }
349
350    fn object_name(&self) -> &str {
351        match self.properties.get(&PropertyIdentifier::OBJECT_NAME) {
352            Some(PropertyValue::CharacterString(s)) => s,
353            _ => "Unknown",
354        }
355    }
356
357    fn read_property(
358        &self,
359        property: PropertyIdentifier,
360        array_index: Option<u32>,
361    ) -> Result<PropertyValue, Error> {
362        if property == PropertyIdentifier::OBJECT_LIST {
363            return match array_index {
364                None => {
365                    let elements = self
366                        .object_list
367                        .iter()
368                        .map(|oid| PropertyValue::ObjectIdentifier(*oid))
369                        .collect();
370                    Ok(PropertyValue::List(elements))
371                }
372                Some(0) => {
373                    // Index 0 = array length per BACnet convention
374                    Ok(PropertyValue::Unsigned(self.object_list.len() as u64))
375                }
376                Some(idx) => {
377                    let i = (idx - 1) as usize; // BACnet arrays are 1-based
378                    if i < self.object_list.len() {
379                        Ok(PropertyValue::ObjectIdentifier(self.object_list[i]))
380                    } else {
381                        Err(Error::Protocol {
382                            class: ErrorClass::PROPERTY.to_raw() as u32,
383                            code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
384                        })
385                    }
386                }
387            };
388        }
389
390        if property == PropertyIdentifier::PROPERTY_LIST {
391            return read_property_list_property(&self.property_list(), array_index);
392        }
393
394        if property == PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED {
395            let num_bytes = self.protocol_object_types_supported.len();
396            let total_bits = num_bytes * 8;
397            // Find highest set bit to determine actual used bits
398            let mut max_type = 0u32;
399            for (byte_idx, &byte) in self.protocol_object_types_supported.iter().enumerate() {
400                for bit in 0..8 {
401                    if byte & (1 << (7 - bit)) != 0 {
402                        max_type = (byte_idx * 8 + bit) as u32;
403                    }
404                }
405            }
406            let used_bits = max_type as usize + 1;
407            let unused = (total_bits - used_bits) as u8;
408            return Ok(PropertyValue::BitString {
409                unused_bits: unused,
410                data: self.protocol_object_types_supported.clone(),
411            });
412        }
413
414        if property == PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED {
415            // 6 bytes = 48 bits; 41 defined (services 0-40), 7 unused bits
416            return Ok(PropertyValue::BitString {
417                unused_bits: 7,
418                data: self.protocol_services_supported.clone(),
419            });
420        }
421
422        if property == PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS {
423            let elements: Vec<PropertyValue> = self
424                .active_cov_subscriptions
425                .iter()
426                .map(|sub| {
427                    let mut entry = vec![
428                        PropertyValue::ObjectIdentifier(
429                            sub.monitored_property_reference.object_identifier,
430                        ),
431                        PropertyValue::Unsigned(sub.recipient.process_identifier as u64),
432                        PropertyValue::Boolean(sub.issue_confirmed_notifications),
433                        PropertyValue::Unsigned(sub.time_remaining as u64),
434                    ];
435                    if let Some(inc) = sub.cov_increment {
436                        entry.push(PropertyValue::Real(inc));
437                    }
438                    PropertyValue::List(entry)
439                })
440                .collect();
441            return Ok(PropertyValue::List(elements));
442        }
443
444        self.properties
445            .get(&property)
446            .cloned()
447            .ok_or(Error::Protocol {
448                class: ErrorClass::PROPERTY.to_raw() as u32,
449                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
450            })
451    }
452
453    fn write_property(
454        &mut self,
455        property: PropertyIdentifier,
456        _array_index: Option<u32>,
457        value: PropertyValue,
458        _priority: Option<u8>,
459    ) -> Result<(), Error> {
460        if property == PropertyIdentifier::DESCRIPTION {
461            if let PropertyValue::CharacterString(_) = &value {
462                self.properties.insert(property, value);
463                return Ok(());
464            }
465            return Err(Error::Protocol {
466                class: ErrorClass::PROPERTY.to_raw() as u32,
467                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
468            });
469        }
470        Err(Error::Protocol {
471            class: ErrorClass::PROPERTY.to_raw() as u32,
472            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
473        })
474    }
475
476    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
477        let mut props: Vec<PropertyIdentifier> = self.properties.keys().copied().collect();
478        props.push(PropertyIdentifier::OBJECT_LIST);
479        props.push(PropertyIdentifier::PROPERTY_LIST);
480        props.push(PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED);
481        props.push(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED);
482        props.push(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS);
483        props.sort_by_key(|p| p.to_raw());
484        Cow::Owned(props)
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    fn make_device() -> DeviceObject {
493        DeviceObject::new(DeviceConfig {
494            instance: 1234,
495            name: "Test Device".into(),
496            ..DeviceConfig::default()
497        })
498        .unwrap()
499    }
500
501    #[test]
502    fn read_object_identifier() {
503        let dev = make_device();
504        let val = dev
505            .read_property(PropertyIdentifier::OBJECT_IDENTIFIER, None)
506            .unwrap();
507        let expected_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
508        assert_eq!(val, PropertyValue::ObjectIdentifier(expected_oid));
509    }
510
511    #[test]
512    fn read_object_name() {
513        let dev = make_device();
514        let val = dev
515            .read_property(PropertyIdentifier::OBJECT_NAME, None)
516            .unwrap();
517        assert_eq!(val, PropertyValue::CharacterString("Test Device".into()));
518    }
519
520    #[test]
521    fn read_object_type() {
522        let dev = make_device();
523        let val = dev
524            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
525            .unwrap();
526        assert_eq!(val, PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()));
527    }
528
529    #[test]
530    fn read_vendor_name() {
531        let dev = make_device();
532        let val = dev
533            .read_property(PropertyIdentifier::VENDOR_NAME, None)
534            .unwrap();
535        assert_eq!(val, PropertyValue::CharacterString("Rusty BACnet".into()));
536    }
537
538    #[test]
539    fn read_max_apdu_length() {
540        let dev = make_device();
541        let val = dev
542            .read_property(PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED, None)
543            .unwrap();
544        assert_eq!(val, PropertyValue::Unsigned(1476));
545    }
546
547    #[test]
548    fn read_unknown_property_fails() {
549        let dev = make_device();
550        // Use a property that Device doesn't have
551        let result = dev.read_property(PropertyIdentifier::PRESENT_VALUE, None);
552        assert!(result.is_err());
553    }
554
555    #[test]
556    fn write_property_denied() {
557        let mut dev = make_device();
558        let result = dev.write_property(
559            PropertyIdentifier::OBJECT_NAME,
560            None,
561            PropertyValue::CharacterString("New Name".into()),
562            None,
563        );
564        assert!(result.is_err());
565    }
566
567    #[test]
568    fn device_description_default_empty() {
569        let dev = make_device();
570        let val = dev
571            .read_property(PropertyIdentifier::DESCRIPTION, None)
572            .unwrap();
573        assert_eq!(val, PropertyValue::CharacterString(String::new()));
574    }
575
576    #[test]
577    fn device_description_write_read() {
578        let mut dev = make_device();
579        dev.write_property(
580            PropertyIdentifier::DESCRIPTION,
581            None,
582            PropertyValue::CharacterString("Main building controller".into()),
583            None,
584        )
585        .unwrap();
586        assert_eq!(
587            dev.read_property(PropertyIdentifier::DESCRIPTION, None)
588                .unwrap(),
589            PropertyValue::CharacterString("Main building controller".into())
590        );
591    }
592
593    #[test]
594    fn device_set_description_convenience() {
595        let mut dev = make_device();
596        dev.set_description("Rooftop unit controller");
597        assert_eq!(
598            dev.read_property(PropertyIdentifier::DESCRIPTION, None)
599                .unwrap(),
600            PropertyValue::CharacterString("Rooftop unit controller".into())
601        );
602    }
603
604    #[test]
605    fn device_description_in_property_list() {
606        let dev = make_device();
607        assert!(dev
608            .property_list()
609            .contains(&PropertyIdentifier::DESCRIPTION));
610    }
611
612    #[test]
613    fn object_list_default_contains_device() {
614        let dev = make_device();
615        // arrayIndex absent: returns the full array as a List
616        let val = dev
617            .read_property(PropertyIdentifier::OBJECT_LIST, None)
618            .unwrap();
619        let expected_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
620        assert_eq!(
621            val,
622            PropertyValue::List(vec![PropertyValue::ObjectIdentifier(expected_oid)])
623        );
624    }
625
626    #[test]
627    fn object_list_array_index() {
628        let dev = make_device();
629        // Index 0 = length
630        let val = dev
631            .read_property(PropertyIdentifier::OBJECT_LIST, Some(0))
632            .unwrap();
633        assert_eq!(val, PropertyValue::Unsigned(1));
634
635        // Index 1 = first element (the device itself)
636        let val = dev
637            .read_property(PropertyIdentifier::OBJECT_LIST, Some(1))
638            .unwrap();
639        let expected_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
640        assert_eq!(val, PropertyValue::ObjectIdentifier(expected_oid));
641
642        // Index 2 = out of range
643        let result = dev.read_property(PropertyIdentifier::OBJECT_LIST, Some(2));
644        assert!(result.is_err());
645    }
646
647    #[test]
648    fn set_object_list() {
649        let mut dev = make_device();
650        let dev_oid = dev.object_identifier();
651        let ai1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
652        let ai2 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap();
653        dev.set_object_list(vec![dev_oid, ai1, ai2]);
654
655        // arrayIndex absent: returns the full array
656        let val = dev
657            .read_property(PropertyIdentifier::OBJECT_LIST, None)
658            .unwrap();
659        assert_eq!(
660            val,
661            PropertyValue::List(vec![
662                PropertyValue::ObjectIdentifier(dev_oid),
663                PropertyValue::ObjectIdentifier(ai1),
664                PropertyValue::ObjectIdentifier(ai2),
665            ])
666        );
667
668        // arrayIndex 0: returns the count
669        let count = dev
670            .read_property(PropertyIdentifier::OBJECT_LIST, Some(0))
671            .unwrap();
672        assert_eq!(count, PropertyValue::Unsigned(3));
673    }
674
675    #[test]
676    fn property_list_contains_expected() {
677        let dev = make_device();
678        let props = dev.property_list();
679        assert!(props.contains(&PropertyIdentifier::OBJECT_IDENTIFIER));
680        assert!(props.contains(&PropertyIdentifier::OBJECT_NAME));
681        assert!(props.contains(&PropertyIdentifier::OBJECT_TYPE));
682        assert!(props.contains(&PropertyIdentifier::VENDOR_NAME));
683        assert!(props.contains(&PropertyIdentifier::OBJECT_LIST));
684        assert!(props.contains(&PropertyIdentifier::PROPERTY_LIST));
685        assert!(props.contains(&PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED));
686        assert!(props.contains(&PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED));
687    }
688
689    #[test]
690    fn read_protocol_object_types_supported() {
691        let dev = make_device();
692        let val = dev
693            .read_property(PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED, None)
694            .unwrap();
695        match val {
696            PropertyValue::BitString { unused_bits, data } => {
697                assert_eq!(unused_bits, 7);
698                assert_eq!(data.len(), 9);
699                // Byte 0 (types 0-7): all set
700                assert_eq!(data[0], 0xFF);
701                // Byte 1 (types 8-15): all set
702                assert_eq!(data[1], 0xFF);
703                // Byte 2 (types 16-23): all set
704                assert_eq!(data[2], 0xFF);
705                // Byte 3 (types 24-31): all set
706                assert_eq!(data[3], 0xFF);
707                // Byte 4 (types 32-39): 32-37,39 set; 38 (NetworkSecurity) unset
708                assert_eq!(data[4], 0xFD);
709                // Byte 5 (types 40-47): all set
710                assert_eq!(data[5], 0xFF);
711                // Byte 6 (types 48-55): all set
712                assert_eq!(data[6], 0xFF);
713                // Byte 7 (types 56-63): all set (56-62 + Color=63)
714                assert_eq!(data[7], 0xFF);
715                // Byte 8 (type 64): ColorTemperature set, 7 unused bits
716                assert_eq!(data[8], 0x80);
717            }
718            _ => panic!("Expected BitString"),
719        }
720    }
721
722    #[test]
723    fn read_protocol_services_supported() {
724        let dev = make_device();
725        let val = dev
726            .read_property(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED, None)
727            .unwrap();
728        match val {
729            PropertyValue::BitString { unused_bits, data } => {
730                assert_eq!(unused_bits, 7);
731                assert_eq!(data.len(), 6);
732                // Byte 0: services 0,2,5
733                assert_eq!(data[0], 0xA4);
734                // Byte 1: services 12,14,15
735                assert_eq!(data[1], 0x0B);
736                // Byte 4: service 32 (WhoIs)
737                assert_eq!(data[4], 0x80);
738            }
739            _ => panic!("Expected BitString"),
740        }
741    }
742
743    #[test]
744    fn active_cov_subscriptions_default_empty() {
745        let dev = make_device();
746        let val = dev
747            .read_property(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS, None)
748            .unwrap();
749        assert_eq!(val, PropertyValue::List(vec![]));
750    }
751
752    #[test]
753    fn active_cov_subscriptions_in_property_list() {
754        let dev = make_device();
755        assert!(dev
756            .property_list()
757            .contains(&PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS));
758    }
759
760    #[test]
761    fn active_cov_subscriptions_after_add() {
762        use bacnet_types::constructed::{
763            BACnetCOVSubscription, BACnetObjectPropertyReference, BACnetRecipient,
764            BACnetRecipientProcess,
765        };
766
767        let mut dev = make_device();
768        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap();
769        let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
770
771        dev.add_cov_subscription(BACnetCOVSubscription {
772            recipient: BACnetRecipientProcess {
773                recipient: BACnetRecipient::Device(dev_oid),
774                process_identifier: 7,
775            },
776            monitored_property_reference: BACnetObjectPropertyReference::new(
777                ai_oid,
778                PropertyIdentifier::PRESENT_VALUE.to_raw(),
779            ),
780            issue_confirmed_notifications: true,
781            time_remaining: 300,
782            cov_increment: Some(0.5),
783        });
784
785        let val = dev
786            .read_property(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS, None)
787            .unwrap();
788        match val {
789            PropertyValue::List(subs) => {
790                assert_eq!(subs.len(), 1);
791                match &subs[0] {
792                    PropertyValue::List(entry) => {
793                        assert_eq!(entry.len(), 5); // includes cov_increment
794                        assert_eq!(entry[0], PropertyValue::ObjectIdentifier(ai_oid));
795                        assert_eq!(entry[1], PropertyValue::Unsigned(7));
796                        assert_eq!(entry[2], PropertyValue::Boolean(true));
797                        assert_eq!(entry[3], PropertyValue::Unsigned(300));
798                        assert_eq!(entry[4], PropertyValue::Real(0.5));
799                    }
800                    _ => panic!("Expected List entry"),
801                }
802            }
803            _ => panic!("Expected List"),
804        }
805    }
806
807    #[test]
808    fn active_cov_subscriptions_without_increment() {
809        use bacnet_types::constructed::{
810            BACnetCOVSubscription, BACnetObjectPropertyReference, BACnetRecipient,
811            BACnetRecipientProcess,
812        };
813
814        let mut dev = make_device();
815        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 50).unwrap();
816        let bv_oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3).unwrap();
817
818        dev.add_cov_subscription(BACnetCOVSubscription {
819            recipient: BACnetRecipientProcess {
820                recipient: BACnetRecipient::Device(dev_oid),
821                process_identifier: 1,
822            },
823            monitored_property_reference: BACnetObjectPropertyReference::new(
824                bv_oid,
825                PropertyIdentifier::PRESENT_VALUE.to_raw(),
826            ),
827            issue_confirmed_notifications: false,
828            time_remaining: 0,
829            cov_increment: None,
830        });
831
832        let val = dev
833            .read_property(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS, None)
834            .unwrap();
835        match val {
836            PropertyValue::List(subs) => {
837                assert_eq!(subs.len(), 1);
838                match &subs[0] {
839                    PropertyValue::List(entry) => {
840                        assert_eq!(entry.len(), 4); // no cov_increment
841                        assert_eq!(entry[2], PropertyValue::Boolean(false));
842                    }
843                    _ => panic!("Expected List entry"),
844                }
845            }
846            _ => panic!("Expected List"),
847        }
848    }
849
850    #[test]
851    fn active_cov_subscriptions_write_denied() {
852        let mut dev = make_device();
853        let result = dev.write_property(
854            PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS,
855            None,
856            PropertyValue::List(vec![]),
857            None,
858        );
859        assert!(result.is_err());
860    }
861
862    #[test]
863    fn set_active_cov_subscriptions_replaces() {
864        use bacnet_types::constructed::{
865            BACnetCOVSubscription, BACnetObjectPropertyReference, BACnetRecipient,
866            BACnetRecipientProcess,
867        };
868
869        let mut dev = make_device();
870        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 10).unwrap();
871        let ai1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
872        let ai2 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap();
873
874        // Add two subscriptions
875        let sub1 = BACnetCOVSubscription {
876            recipient: BACnetRecipientProcess {
877                recipient: BACnetRecipient::Device(dev_oid),
878                process_identifier: 1,
879            },
880            monitored_property_reference: BACnetObjectPropertyReference::new(
881                ai1,
882                PropertyIdentifier::PRESENT_VALUE.to_raw(),
883            ),
884            issue_confirmed_notifications: true,
885            time_remaining: 100,
886            cov_increment: None,
887        };
888        let sub2 = BACnetCOVSubscription {
889            recipient: BACnetRecipientProcess {
890                recipient: BACnetRecipient::Device(dev_oid),
891                process_identifier: 2,
892            },
893            monitored_property_reference: BACnetObjectPropertyReference::new(
894                ai2,
895                PropertyIdentifier::PRESENT_VALUE.to_raw(),
896            ),
897            issue_confirmed_notifications: false,
898            time_remaining: 200,
899            cov_increment: Some(1.0),
900        };
901        dev.set_active_cov_subscriptions(vec![sub1, sub2]);
902
903        let val = dev
904            .read_property(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS, None)
905            .unwrap();
906        match val {
907            PropertyValue::List(subs) => assert_eq!(subs.len(), 2),
908            _ => panic!("Expected List"),
909        }
910
911        // Replace with empty
912        dev.set_active_cov_subscriptions(vec![]);
913        let val = dev
914            .read_property(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS, None)
915            .unwrap();
916        assert_eq!(val, PropertyValue::List(vec![]));
917    }
918
919    #[test]
920    fn compute_object_types_supported_known_inputs() {
921        assert_eq!(compute_object_types_supported(&[0]), vec![0x80]);
922        assert_eq!(compute_object_types_supported(&[8]), vec![0x00, 0x80]);
923        assert_eq!(
924            compute_object_types_supported(&[0, 1, 2, 3, 4, 5]),
925            vec![0xFC]
926        );
927        assert_eq!(compute_object_types_supported(&[]), vec![0x00]);
928    }
929
930    #[test]
931    fn compute_object_types_supported_old_bits_preserved() {
932        let old_types: Vec<u32> = vec![0, 1, 2, 3, 4, 5, 8, 13, 14, 19];
933        let bs = compute_object_types_supported(&old_types);
934        assert_eq!(bs[0], 0xFC);
935        assert_eq!(bs[1], 0x86);
936        assert_eq!(bs[2], 0x10);
937    }
938
939    #[test]
940    fn device_protocol_object_types_has_new_bits() {
941        let dev = DeviceObject::new(DeviceConfig {
942            instance: 1,
943            name: "Test".into(),
944            ..DeviceConfig::default()
945        })
946        .unwrap();
947        let val = dev
948            .read_property(PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED, None)
949            .unwrap();
950        let bits = match val {
951            PropertyValue::BitString { data, .. } => data,
952            _ => panic!("Expected BitString"),
953        };
954        assert!(bits.len() >= 8, "bitstring should cover types up to 62");
955        assert_eq!(bits[0] & 0xFC, 0xFC, "AI/AO/AV/BI/BO/BV");
956        assert_ne!(bits[1] & 0x80, 0, "Device (8)");
957        assert_ne!(bits[1] & 0x04, 0, "MSI (13)");
958        assert_ne!(bits[1] & 0x02, 0, "MSO (14)");
959        assert_ne!(bits[2] & 0x10, 0, "MSV (19)");
960        assert_ne!(bits[0] & 0x03, 0, "Calendar(6) and Command(7)");
961        assert_ne!(bits[3] & 0x80, 0, "Accumulator (24)");
962        assert_ne!(bits[7] & 0x80, 0, "NetworkPort (56)");
963    }
964}