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