Skip to main content

bacnet_objects/device/
mod.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;
9use std::sync::Arc;
10
11use bacnet_types::enums::{
12    ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Segmentation, ServiceSupported,
13};
14use bacnet_types::error::Error;
15use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
16
17use crate::clock::{ClockFrame, ClockReader};
18use crate::common::read_property_list_property;
19use crate::traits::BACnetObject;
20
21mod audit_recipient;
22mod metadata;
23pub use audit_recipient::{AuditRecipientChangeSink, AuditWriteSource, DeviceAuthority};
24
25/// Every service the bundled `bacnet-server` dispatch executes, as
26/// `BACnetServicesSupported` bit positions (Clause 21).
27///
28/// Default source for `Protocol_Services_Supported`, which Clause 12.11 ties
29/// to services *executed* — the server's initiate-only services (I-Am,
30/// I-Have, COV/event notifications) are deliberately absent. Kept in lockstep
31/// with the dispatch arms by `bacnet-server`'s executed-services cross-check
32/// test; deployments with a different dispatch surface override via
33/// [`DeviceObject::set_services_supported`].
34pub const EXECUTED_SERVICES: &[ServiceSupported] = &[
35    ServiceSupported::ACKNOWLEDGE_ALARM,
36    // Received notifications go to the Notification Forwarder objects
37    // (Clause 12.51); the server acknowledges each confirmed one.
38    ServiceSupported::CONFIRMED_EVENT_NOTIFICATION,
39    ServiceSupported::GET_ALARM_SUMMARY,
40    ServiceSupported::GET_ENROLLMENT_SUMMARY,
41    ServiceSupported::SUBSCRIBE_COV,
42    ServiceSupported::ATOMIC_READ_FILE,
43    ServiceSupported::ATOMIC_WRITE_FILE,
44    ServiceSupported::ADD_LIST_ELEMENT,
45    ServiceSupported::REMOVE_LIST_ELEMENT,
46    ServiceSupported::CREATE_OBJECT,
47    ServiceSupported::DELETE_OBJECT,
48    ServiceSupported::READ_PROPERTY,
49    ServiceSupported::READ_PROPERTY_MULTIPLE,
50    ServiceSupported::WRITE_PROPERTY,
51    ServiceSupported::WRITE_PROPERTY_MULTIPLE,
52    ServiceSupported::DEVICE_COMMUNICATION_CONTROL,
53    ServiceSupported::CONFIRMED_TEXT_MESSAGE,
54    ServiceSupported::REINITIALIZE_DEVICE,
55    ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION,
56    ServiceSupported::UNCONFIRMED_TEXT_MESSAGE,
57    ServiceSupported::TIME_SYNCHRONIZATION,
58    ServiceSupported::WHO_HAS,
59    ServiceSupported::WHO_IS,
60    ServiceSupported::READ_RANGE,
61    ServiceSupported::UTC_TIME_SYNCHRONIZATION,
62    // Executes authorized silence/unsilence operations. Built-in reset execution
63    // stays unsupported pending application-executor and replay semantics.
64    ServiceSupported::LIFE_SAFETY_OPERATION,
65    ServiceSupported::SUBSCRIBE_COV_PROPERTY,
66    ServiceSupported::GET_EVENT_INFORMATION,
67    // Applied to the local Channel objects (Clause 15.11).
68    ServiceSupported::WRITE_GROUP,
69    ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE,
70    ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION,
71    ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION,
72    ServiceSupported::AUDIT_LOG_QUERY,
73];
74
75/// Number of bits in the `BACnetServicesSupported` production: bits 0..=48
76/// (you-Are is the highest defined bit, Clause 21).
77const SERVICES_SUPPORTED_BITS: usize = 49;
78
79/// Build the `Protocol_Services_Supported` bit string from a service set,
80/// sized for the full production (7 octets, 7 unused bits) and packed
81/// MSB-first per Clause 20.2.10: bit N at byte N/8, position 7-(N%8).
82fn compute_services_supported(services: &[ServiceSupported]) -> Vec<u8> {
83    let mut bits = vec![0u8; SERVICES_SUPPORTED_BITS.div_ceil(8)];
84    for service in services {
85        let n = service.to_raw() as usize;
86        if n < SERVICES_SUPPORTED_BITS {
87            bits[n / 8] |= 0x80 >> (n % 8);
88        }
89    }
90    bits
91}
92
93/// Build a BACnet bitstring representing supported object types.
94/// Each type N sets bit at byte N/8, position 7-(N%8) (MSB-first within each byte).
95fn compute_object_types_supported(types: &[u32]) -> Vec<u8> {
96    let max_type = types.iter().copied().max().unwrap_or(0) as usize;
97    let num_bytes = (max_type / 8) + 1;
98    let mut bitstring = vec![0u8; num_bytes];
99    for &t in types {
100        let byte_idx = (t as usize) / 8;
101        let bit_pos = 7 - ((t as usize) % 8);
102        if byte_idx < bitstring.len() {
103            bitstring[byte_idx] |= 1 << bit_pos;
104        }
105    }
106    bitstring
107}
108
109/// Configuration for creating a Device object.
110pub struct DeviceConfig {
111    /// Device instance number (0..4194303).
112    pub instance: u32,
113    /// Device object name.
114    pub name: String,
115    /// Vendor name string.
116    pub vendor_name: String,
117    /// ASHRAE-assigned vendor identifier.
118    pub vendor_id: u16,
119    /// Model name string.
120    pub model_name: String,
121    /// Firmware revision string.
122    pub firmware_revision: String,
123    /// Application software version string.
124    pub application_software_version: String,
125    /// Maximum APDU length accepted (typically 1476 for BIP).
126    pub max_apdu_length: u32,
127    /// Segmentation support level.
128    pub segmentation_supported: Segmentation,
129    /// APDU timeout in milliseconds.
130    pub apdu_timeout: u32,
131    /// Number of APDU retries.
132    pub apdu_retries: u32,
133}
134
135impl Default for DeviceConfig {
136    fn default() -> Self {
137        Self {
138            instance: 1,
139            name: "BACnet Device".into(),
140            vendor_name: "Rusty BACnet".into(),
141            vendor_id: 0,
142            model_name: "rusty-bacnet".into(),
143            firmware_revision: "0.1.0".into(),
144            application_software_version: "0.1.0".into(),
145            max_apdu_length: 1476,
146            segmentation_supported: Segmentation::NONE,
147            apdu_timeout: 6000,
148            apdu_retries: 3,
149        }
150    }
151}
152
153/// BACnet Device object.
154pub struct DeviceObject {
155    oid: ObjectIdentifier,
156    recipient: audit_recipient::RecipientState,
157    properties: HashMap<PropertyIdentifier, PropertyValue>,
158    /// Cached object list for array-indexed reads.
159    object_list: Vec<ObjectIdentifier>,
160    /// Protocol_Object_Types_Supported — bitstring indicating which object
161    /// types this device supports (one bit per type, MSB-first within each byte).
162    protocol_object_types_supported: Vec<u8>,
163    /// Configured executed services before clock-availability filtering.
164    configured_services_supported: Vec<ServiceSupported>,
165    /// Shared dynamic clock sample source. `None` is explicitly clockless.
166    clock: Option<Arc<dyn ClockReader>>,
167}
168
169impl DeviceObject {
170    /// Create a new Device object from configuration.
171    pub fn new(config: DeviceConfig) -> Result<Self, Error> {
172        let oid = ObjectIdentifier::new(ObjectType::DEVICE, config.instance)?;
173        let mut properties = HashMap::new();
174
175        properties.insert(
176            PropertyIdentifier::OBJECT_IDENTIFIER,
177            PropertyValue::ObjectIdentifier(oid),
178        );
179        properties.insert(
180            PropertyIdentifier::OBJECT_NAME,
181            PropertyValue::CharacterString(config.name),
182        );
183        properties.insert(
184            PropertyIdentifier::OBJECT_TYPE,
185            PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()),
186        );
187        properties.insert(
188            PropertyIdentifier::SYSTEM_STATUS,
189            PropertyValue::Enumerated(0), // operational
190        );
191        properties.insert(
192            PropertyIdentifier::VENDOR_NAME,
193            PropertyValue::CharacterString(config.vendor_name),
194        );
195        properties.insert(
196            PropertyIdentifier::VENDOR_IDENTIFIER,
197            PropertyValue::Unsigned(config.vendor_id as u64),
198        );
199        properties.insert(
200            PropertyIdentifier::MODEL_NAME,
201            PropertyValue::CharacterString(config.model_name),
202        );
203        properties.insert(
204            PropertyIdentifier::FIRMWARE_REVISION,
205            PropertyValue::CharacterString(config.firmware_revision),
206        );
207        properties.insert(
208            PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
209            PropertyValue::CharacterString(config.application_software_version),
210        );
211        properties.insert(
212            PropertyIdentifier::PROTOCOL_VERSION,
213            PropertyValue::Unsigned(1),
214        );
215        properties.insert(
216            PropertyIdentifier::PROTOCOL_REVISION,
217            PropertyValue::Unsigned(22), // Revision 22 (2020)
218        );
219        properties.insert(
220            PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
221            PropertyValue::Unsigned(config.max_apdu_length as u64),
222        );
223        properties.insert(
224            PropertyIdentifier::SEGMENTATION_SUPPORTED,
225            PropertyValue::Enumerated(config.segmentation_supported.to_raw() as u32),
226        );
227        properties.insert(
228            PropertyIdentifier::APDU_TIMEOUT,
229            PropertyValue::Unsigned(config.apdu_timeout as u64),
230        );
231        properties.insert(
232            PropertyIdentifier::NUMBER_OF_APDU_RETRIES,
233            PropertyValue::Unsigned(config.apdu_retries as u64),
234        );
235        properties.insert(
236            PropertyIdentifier::DATABASE_REVISION,
237            PropertyValue::Unsigned(0),
238        );
239        properties.insert(
240            PropertyIdentifier::DESCRIPTION,
241            PropertyValue::CharacterString(String::new()),
242        );
243
244        // Device_Address_Binding: stored empty. A running server serves its
245        // device bindings in place of this value (bacnet-server, #1369).
246        properties.insert(
247            PropertyIdentifier::DEVICE_ADDRESS_BINDING,
248            PropertyValue::List(Vec::new()),
249        );
250
251        // Last_Restart_Reason: 0=unknown, 1=coldstart, 2=warmstart, etc.
252        properties.insert(
253            PropertyIdentifier::LAST_RESTART_REASON,
254            PropertyValue::Enumerated(0), // unknown
255        );
256
257        // Device_UUID: 16-byte UUID stored as OctetString. Default: all zeros.
258        properties.insert(
259            PropertyIdentifier::DEVICE_UUID,
260            PropertyValue::OctetString(vec![0u8; 16]),
261        );
262
263        // Max_Segments_Accepted — only included when segmentation is supported.
264        if config.segmentation_supported != Segmentation::NONE {
265            let max_segments_accepted = if config.segmentation_supported == Segmentation::TRANSMIT {
266                1
267            } else {
268                65
269            };
270            properties.insert(
271                PropertyIdentifier::MAX_SEGMENTS_ACCEPTED,
272                PropertyValue::Unsigned(max_segments_accepted),
273            );
274        }
275
276        // Protocol_Object_Types_Supported: bitstring with one bit per
277        // implemented object type.  Computed from the full set of types
278        // that have concrete struct implementations in this crate.
279        let protocol_object_types_supported = compute_object_types_supported(&[
280            ObjectType::ANALOG_INPUT.to_raw(),
281            ObjectType::ANALOG_OUTPUT.to_raw(),
282            ObjectType::ANALOG_VALUE.to_raw(),
283            ObjectType::BINARY_INPUT.to_raw(),
284            ObjectType::BINARY_OUTPUT.to_raw(),
285            ObjectType::BINARY_VALUE.to_raw(),
286            ObjectType::CALENDAR.to_raw(),
287            ObjectType::COMMAND.to_raw(),
288            ObjectType::DEVICE.to_raw(),
289            ObjectType::EVENT_ENROLLMENT.to_raw(),
290            ObjectType::FILE.to_raw(),
291            ObjectType::GROUP.to_raw(),
292            ObjectType::LOOP.to_raw(),
293            ObjectType::MULTI_STATE_INPUT.to_raw(),
294            ObjectType::MULTI_STATE_OUTPUT.to_raw(),
295            ObjectType::NOTIFICATION_CLASS.to_raw(),
296            ObjectType::PROGRAM.to_raw(),
297            ObjectType::SCHEDULE.to_raw(),
298            ObjectType::AVERAGING.to_raw(),
299            ObjectType::MULTI_STATE_VALUE.to_raw(),
300            ObjectType::TREND_LOG.to_raw(),
301            ObjectType::LIFE_SAFETY_POINT.to_raw(),
302            ObjectType::LIFE_SAFETY_ZONE.to_raw(),
303            ObjectType::ACCUMULATOR.to_raw(),
304            ObjectType::PULSE_CONVERTER.to_raw(),
305            ObjectType::EVENT_LOG.to_raw(),
306            ObjectType::GLOBAL_GROUP.to_raw(),
307            ObjectType::TREND_LOG_MULTIPLE.to_raw(),
308            ObjectType::LOAD_CONTROL.to_raw(),
309            ObjectType::STRUCTURED_VIEW.to_raw(),
310            ObjectType::ACCESS_DOOR.to_raw(),
311            ObjectType::TIMER.to_raw(),
312            ObjectType::ACCESS_CREDENTIAL.to_raw(),
313            ObjectType::ACCESS_POINT.to_raw(),
314            ObjectType::ACCESS_RIGHTS.to_raw(),
315            ObjectType::ACCESS_USER.to_raw(),
316            ObjectType::ACCESS_ZONE.to_raw(),
317            ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
318            ObjectType::BITSTRING_VALUE.to_raw(),
319            ObjectType::CHARACTERSTRING_VALUE.to_raw(),
320            ObjectType::DATEPATTERN_VALUE.to_raw(),
321            ObjectType::DATE_VALUE.to_raw(),
322            ObjectType::DATETIMEPATTERN_VALUE.to_raw(),
323            ObjectType::DATETIME_VALUE.to_raw(),
324            ObjectType::INTEGER_VALUE.to_raw(),
325            ObjectType::LARGE_ANALOG_VALUE.to_raw(),
326            ObjectType::OCTETSTRING_VALUE.to_raw(),
327            ObjectType::POSITIVE_INTEGER_VALUE.to_raw(),
328            ObjectType::TIMEPATTERN_VALUE.to_raw(),
329            ObjectType::TIME_VALUE.to_raw(),
330            ObjectType::NOTIFICATION_FORWARDER.to_raw(),
331            ObjectType::ALERT_ENROLLMENT.to_raw(),
332            ObjectType::CHANNEL.to_raw(),
333            ObjectType::LIGHTING_OUTPUT.to_raw(),
334            ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
335            ObjectType::NETWORK_PORT.to_raw(),
336            ObjectType::ELEVATOR_GROUP.to_raw(),
337            ObjectType::ESCALATOR.to_raw(),
338            ObjectType::LIFT.to_raw(),
339            ObjectType::STAGING.to_raw(),
340            ObjectType::AUDIT_LOG.to_raw(),
341            ObjectType::AUDIT_REPORTER.to_raw(),
342            ObjectType::COLOR.to_raw(),
343            ObjectType::COLOR_TEMPERATURE.to_raw(),
344        ]);
345
346        Ok(Self {
347            oid,
348            recipient: Default::default(),
349            properties,
350            object_list: vec![oid], // Device itself is always in the list
351            protocol_object_types_supported,
352            configured_services_supported: EXECUTED_SERVICES.to_vec(),
353            clock: None,
354        })
355    }
356
357    /// Update the object-list with the current database contents.
358    pub fn set_object_list(&mut self, oids: Vec<ObjectIdentifier>) {
359        self.object_list = oids;
360    }
361
362    /// Replace the advertised executed-service set (`Protocol_Services_Supported`,
363    /// Clause 12.11). For deployments whose dispatch surface differs from the
364    /// bundled server's [`EXECUTED_SERVICES`]. This is a standalone declaration,
365    /// not a runtime dispatch switch. Raw COV presence follows these services;
366    /// a running server overrides served Device data with its own execution view.
367    ///
368    /// The production is closed at you-Are (bit 48); values past it are not
369    /// representable and are dropped.
370    pub fn set_services_supported(&mut self, services: &[ServiceSupported]) {
371        self.configured_services_supported = services.to_vec();
372    }
373
374    /// Get the device instance number.
375    pub fn instance(&self) -> u32 {
376        self.oid.instance_number()
377    }
378
379    /// Set the description string.
380    pub fn set_description(&mut self, desc: impl Into<String>) {
381        self.properties.insert(
382            PropertyIdentifier::DESCRIPTION,
383            PropertyValue::CharacterString(desc.into()),
384        );
385    }
386
387    /// Sync the durable Device UUID (Clause 12.11 DEVICE_UUID).
388    ///
389    /// RB-16 endpoint sync (not a fork): the caller owns the 16-byte UUID
390    /// durably (Annex AB.1.5.3); this setter only stores the supplied bytes
391    /// as an OctetString. No generation, no persistence, no version/variant
392    /// checks — startup validation elsewhere rejects only all-zero where a
393    /// real SC identity is required.
394    pub fn set_device_uuid(&mut self, uuid: [u8; 16]) {
395        self.properties.insert(
396            PropertyIdentifier::DEVICE_UUID,
397            PropertyValue::OctetString(uuid.to_vec()),
398        );
399    }
400
401    fn clock_frame(&self) -> Option<ClockFrame> {
402        self.clock.as_ref()?.read_clock()
403    }
404
405    fn cov_property_present(&self, property: PropertyIdentifier) -> bool {
406        match property {
407            PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS => {
408                self.configured_services_supported.iter().any(|service| {
409                    matches!(
410                        *service,
411                        ServiceSupported::SUBSCRIBE_COV | ServiceSupported::SUBSCRIBE_COV_PROPERTY
412                    )
413                })
414            }
415            PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS => self
416                .configured_services_supported
417                .contains(&ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE),
418            _ => true,
419        }
420    }
421
422    fn services_supported(&self) -> Vec<u8> {
423        let clock_available = self.clock_frame().is_some();
424        let services = self
425            .configured_services_supported
426            .iter()
427            .copied()
428            .filter(|service| {
429                clock_available
430                    || (*service != ServiceSupported::TIME_SYNCHRONIZATION
431                        && *service != ServiceSupported::UTC_TIME_SYNCHRONIZATION)
432            })
433            .collect::<Vec<_>>();
434        compute_services_supported(&services)
435    }
436}
437
438impl BACnetObject for DeviceObject {
439    fn device_authority_internal(&mut self) -> Option<DeviceAuthority<'_>> {
440        Some(DeviceAuthority(self))
441    }
442
443    fn object_identifier(&self) -> ObjectIdentifier {
444        self.oid
445    }
446
447    fn object_name(&self) -> &str {
448        match self.properties.get(&PropertyIdentifier::OBJECT_NAME) {
449            Some(PropertyValue::CharacterString(s)) => s,
450            _ => "Unknown",
451        }
452    }
453
454    fn read_property(
455        &self,
456        property: PropertyIdentifier,
457        array_index: Option<u32>,
458    ) -> Result<PropertyValue, Error> {
459        if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
460            return self.read_audit_recipient(array_index);
461        }
462        if property == PropertyIdentifier::OBJECT_LIST {
463            return match array_index {
464                None => {
465                    let elements = self
466                        .object_list
467                        .iter()
468                        .map(|oid| PropertyValue::ObjectIdentifier(*oid))
469                        .collect();
470                    Ok(PropertyValue::List(elements))
471                }
472                Some(0) => {
473                    // Index 0 = array length per BACnet convention
474                    Ok(PropertyValue::Unsigned(self.object_list.len() as u64))
475                }
476                Some(idx) => {
477                    let i = (idx - 1) as usize; // BACnet arrays are 1-based
478                    if i < self.object_list.len() {
479                        Ok(PropertyValue::ObjectIdentifier(self.object_list[i]))
480                    } else {
481                        Err(Error::Protocol {
482                            class: ErrorClass::PROPERTY.to_raw() as u32,
483                            code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
484                        })
485                    }
486                }
487            };
488        }
489
490        if property == PropertyIdentifier::PROPERTY_LIST {
491            return read_property_list_property(&self.property_list(), array_index);
492        }
493
494        if matches!(
495            property,
496            PropertyIdentifier::LOCAL_DATE
497                | PropertyIdentifier::LOCAL_TIME
498                | PropertyIdentifier::UTC_OFFSET
499                | PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS
500        ) {
501            let frame = self.clock_frame().ok_or(Error::Protocol {
502                class: ErrorClass::PROPERTY.to_raw() as u32,
503                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
504            })?;
505            return Ok(match property {
506                PropertyIdentifier::LOCAL_DATE => PropertyValue::Date(frame.local_date),
507                PropertyIdentifier::LOCAL_TIME => PropertyValue::Time(frame.local_time),
508                PropertyIdentifier::UTC_OFFSET => {
509                    PropertyValue::Signed(i32::from(frame.utc_offset))
510                }
511                PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS => {
512                    PropertyValue::Boolean(frame.daylight_savings_status)
513                }
514                _ => unreachable!(),
515            });
516        }
517
518        if property == PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED {
519            let num_bytes = self.protocol_object_types_supported.len();
520            let total_bits = num_bytes * 8;
521            // Find highest set bit to determine actual used bits
522            let mut max_type = 0u32;
523            for (byte_idx, &byte) in self.protocol_object_types_supported.iter().enumerate() {
524                for bit in 0..8 {
525                    if byte & (1 << (7 - bit)) != 0 {
526                        max_type = (byte_idx * 8 + bit) as u32;
527                    }
528                }
529            }
530            let used_bits = max_type as usize + 1;
531            let unused = (total_bits - used_bits) as u8;
532            return Ok(PropertyValue::BitString {
533                unused_bits: unused,
534                data: self.protocol_object_types_supported.clone(),
535            });
536        }
537
538        if property == PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED {
539            let services_supported = self.services_supported();
540            let unused = (services_supported.len() * 8 - SERVICES_SUPPORTED_BITS) as u8;
541            return Ok(PropertyValue::BitString {
542                unused_bits: unused,
543                data: services_supported,
544            });
545        }
546
547        if matches!(
548            property,
549            PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS
550                | PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS
551        ) {
552            // Standalone default: the object holds no subscription state. A
553            // running `BACnetServer` owns both live lists in its COV
554            // subscription table and projects them for network reads and
555            // `read_local`.
556            return if self.cov_property_present(property) {
557                Ok(PropertyValue::ApplicationData(Vec::new()))
558            } else {
559                Err(Error::Protocol {
560                    class: ErrorClass::PROPERTY.to_raw() as u32,
561                    code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
562                })
563            };
564        }
565
566        self.properties
567            .get(&property)
568            .cloned()
569            .ok_or(Error::Protocol {
570                class: ErrorClass::PROPERTY.to_raw() as u32,
571                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
572            })
573    }
574
575    fn write_property(
576        &mut self,
577        property: PropertyIdentifier,
578        array_index: Option<u32>,
579        value: PropertyValue,
580        _priority: Option<u8>,
581    ) -> Result<(), Error> {
582        if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
583            return self.write_audit_recipient(array_index, value, _priority, None);
584        }
585        if property == PropertyIdentifier::DESCRIPTION {
586            if array_index.is_some() {
587                return Err(Error::Protocol {
588                    class: ErrorClass::PROPERTY.to_raw() as u32,
589                    code: ErrorCode::PROPERTY_IS_NOT_AN_ARRAY.to_raw() as u32,
590                });
591            }
592            // Clause 15.9: relinquishing a non-commandable writable property
593            // succeeds without changing its value when no other error exists.
594            if value == PropertyValue::Null {
595                return Ok(());
596            }
597            if let PropertyValue::CharacterString(_) = &value {
598                self.properties.insert(property, value);
599                return Ok(());
600            }
601            return Err(Error::Protocol {
602                class: ErrorClass::PROPERTY.to_raw() as u32,
603                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
604            });
605        }
606        Err(crate::common::unhandled_write_error(
607            self.property_metadata().as_ref(),
608            property,
609            array_index,
610        ))
611    }
612
613    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
614        metadata::for_object(self)
615    }
616
617    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
618        // Preserve Device's legacy inclusion of PROPERTY_LIST. The wire reader
619        // still removes it and the identity rows as before.
620        Cow::Owned(
621            self.property_metadata()
622                .iter()
623                .map(|row| row.property_identifier)
624                .collect(),
625        )
626    }
627
628    /// Device is not createable or deleteable at runtime.
629    fn is_createable(&self) -> bool {
630        false
631    }
632    fn is_deleteable(&self) -> bool {
633        false
634    }
635
636    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
637        self.clock = clock;
638    }
639}
640
641#[cfg(test)]
642mod tests;
643
644#[cfg(test)]
645mod service_profile_tests;