Skip to main content

bacnet_objects/
group.rs

1//! Group, GlobalGroup, and StructuredView objects per ASHRAE 135-2020.
2//!
3//! - GroupObject (type 11) — Clause 12.14
4//! - GlobalGroupObject (type 26) — Clause 12.50
5//! - StructuredViewObject (type 29) — Clause 12.29
6
7use bacnet_encoding::constructed::{
8    encode_property_access_result, encode_read_access_specification,
9};
10use bacnet_types::bitstring::status_flags_from_bacnet;
11use bacnet_types::constructed::{
12    AccessResult, BACnetDeviceObjectPropertyReference, BACnetDeviceObjectReference,
13    BACnetPropertyAccessResult, ReadAccessSpecification,
14};
15use bacnet_types::enums::{EventState, ObjectType, PropertyIdentifier, Reliability};
16use bacnet_types::error::Error;
17use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
18use bytes::BytesMut;
19use std::borrow::Cow;
20
21use crate::common::{self, read_common_properties, read_identity_properties};
22use crate::device_reference;
23use crate::traits::BACnetObject;
24
25mod metadata;
26
27// ---------------------------------------------------------------------------
28// GroupObject (type 11)
29// ---------------------------------------------------------------------------
30
31/// Why [`GroupObject::add_member`] refused a member.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum GroupMemberRefusal {
34    /// The specification lists no property references.
35    NoProperties,
36    /// The specification names a Group or Global Group and selects its
37    /// Present_Value, by name or through ALL or REQUIRED.
38    NestsGroupPresentValue,
39}
40
41impl std::fmt::Display for GroupMemberRefusal {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::NoProperties => f.write_str("the member lists no properties"),
45            Self::NestsGroupPresentValue => f.write_str(
46                "the member reports a Group or Global Group's Present_Value, \
47                 by name or through ALL or REQUIRED",
48            ),
49        }
50    }
51}
52
53impl std::error::Error for GroupMemberRefusal {}
54
55impl From<GroupMemberRefusal> for Error {
56    fn from(_: GroupMemberRefusal) -> Self {
57        common::value_out_of_range_error()
58    }
59}
60
61/// BACnet Group object (type 11).
62///
63/// Each member is a [`ReadAccessSpecification`]: an object in this device and
64/// the properties of it the group reports (Clause 12.14.5). The specification
65/// has no device member, so the Group can't name an object elsewhere; a
66/// Global Group does that.
67///
68/// The object holds no Present_Value. Clause 12.14.6 has it rebuilt from the
69/// members on every read, and only the server, which holds the database, can
70/// read them: it answers Present_Value with one `ReadAccessResult` per
71/// member, read as ReadPropertyMultiple would read that specification. A
72/// direct [`read_property`](BACnetObject::read_property) call on the object
73/// alone returns an empty list.
74pub struct GroupObject {
75    oid: ObjectIdentifier,
76    name: String,
77    description: String,
78    list_of_group_members: Vec<ReadAccessSpecification>,
79}
80
81impl GroupObject {
82    /// Create a new Group object.
83    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
84        let oid = ObjectIdentifier::new(ObjectType::GROUP, instance)?;
85        Ok(Self {
86            oid,
87            name: name.into(),
88            description: String::new(),
89            list_of_group_members: Vec::new(),
90        })
91    }
92
93    /// Append a member to List_Of_Group_Members.
94    ///
95    /// Refused, leaving the members as they were, with the
96    /// [`GroupMemberRefusal`] naming the rule the specification breaks: it
97    /// lists no properties, or names a Group or Global Group and selects that
98    /// object's Present_Value, explicitly or through ALL or REQUIRED (Clause
99    /// 12.14.5 doesn't let one group report another group's Present_Value). A
100    /// refusal converts to PROPERTY / VALUE_OUT_OF_RANGE as an [`Error`]. Any
101    /// property identifier is taken: ASHRAE assigns some above 4194303 (Table
102    /// 23-1), such as Default_Color.
103    pub fn add_member(
104        &mut self,
105        member: ReadAccessSpecification,
106    ) -> Result<(), GroupMemberRefusal> {
107        let references = &member.list_of_property_references;
108        if references.is_empty() {
109            return Err(GroupMemberRefusal::NoProperties);
110        }
111        let nests_a_group = matches!(
112            member.object_identifier.object_type(),
113            ObjectType::GROUP | ObjectType::GLOBAL_GROUP
114        ) && references.iter().any(|reference| {
115            matches!(
116                reference.property_identifier,
117                PropertyIdentifier::PRESENT_VALUE
118                    | PropertyIdentifier::ALL
119                    | PropertyIdentifier::REQUIRED
120            )
121        });
122        if nests_a_group {
123            return Err(GroupMemberRefusal::NestsGroupPresentValue);
124        }
125        self.list_of_group_members.push(member);
126        Ok(())
127    }
128
129    /// The members, in List_Of_Group_Members order.
130    pub fn members(&self) -> &[ReadAccessSpecification] {
131        &self.list_of_group_members
132    }
133
134    /// Clear all members from the group.
135    pub fn clear_members(&mut self) {
136        self.list_of_group_members.clear();
137    }
138}
139
140impl BACnetObject for GroupObject {
141    fn object_identifier(&self) -> ObjectIdentifier {
142        self.oid
143    }
144
145    fn object_name(&self) -> &str {
146        &self.name
147    }
148
149    fn read_property(
150        &self,
151        property: PropertyIdentifier,
152        array_index: Option<u32>,
153    ) -> Result<PropertyValue, Error> {
154        // Table 12-17 has no Status_Flags, Reliability or Out_Of_Service (#1064).
155        if let Some(result) = read_identity_properties!(self, property, array_index) {
156            return result;
157        }
158        match property {
159            p if p == PropertyIdentifier::OBJECT_TYPE => {
160                Ok(PropertyValue::Enumerated(ObjectType::GROUP.to_raw()))
161            }
162            // Both are BACnetLISTs (Table 12-17): an index is refused here as
163            // the service handlers refuse it.
164            p if (p == PropertyIdentifier::LIST_OF_GROUP_MEMBERS
165                || p == PropertyIdentifier::PRESENT_VALUE)
166                && array_index.is_some() =>
167            {
168                Err(common::property_is_not_an_array_error())
169            }
170            // One ReadAccessSpecification per element (Clause 21).
171            p if p == PropertyIdentifier::LIST_OF_GROUP_MEMBERS => Ok(PropertyValue::List(
172                self.list_of_group_members
173                    .iter()
174                    .map(|member| {
175                        let mut encoded = BytesMut::new();
176                        encode_read_access_specification(&mut encoded, member);
177                        PropertyValue::ApplicationData(encoded.to_vec())
178                    })
179                    .collect(),
180            )),
181            // Rebuilt by the server on each read; see the type's docs.
182            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::List(Vec::new())),
183            _ => Err(common::unknown_property_error()),
184        }
185    }
186
187    fn write_property(
188        &mut self,
189        property: PropertyIdentifier,
190        _array_index: Option<u32>,
191        value: PropertyValue,
192        _priority: Option<u8>,
193    ) -> Result<(), Error> {
194        if let Some(result) = common::write_description(&mut self.description, property, &value) {
195            return result;
196        }
197        Err(crate::common::unhandled_write_error(
198            self.property_metadata().as_ref(),
199            property,
200            _array_index,
201        ))
202    }
203
204    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
205        metadata::for_group_object(self)
206    }
207
208    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
209        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
210    }
211}
212
213// ---------------------------------------------------------------------------
214// GlobalGroupObject (type 26)
215// ---------------------------------------------------------------------------
216
217/// BACnet GlobalGroup object (type 26).
218///
219/// Similar to Group but members are DeviceObjectPropertyReference entries,
220/// allowing references to properties on remote devices. GROUP_MEMBER_NAMES
221/// provides human-readable names for each member.
222///
223/// The application sets the members with
224/// [`set_group_members`](Self::set_group_members), acquires their values and
225/// stores what each read produced in `present_value`, by position in the
226/// members: the value, or the error the read failed with. Present_Value goes out as one
227/// `BACnetPropertyAccessResult` per member, the member's reference followed
228/// by that result (Clause 12.50.7), so the array always has one element per
229/// member. A member with no stored result reads as PROPERTY /
230/// VALUE_NOT_INITIALIZED, the result Clause 12.50.7.1 gives a new element,
231/// and stored results past the last member are not served.
232/// Member_Status_Flags is derived from the same store on every read (see
233/// [`member_status_flags`](Self::member_status_flags)), so it tracks
234/// Present_Value without a second update path. Event_State reads NORMAL: the
235/// object has no intrinsic reporting (Clause 12.50.9).
236pub struct GlobalGroupObject {
237    oid: ObjectIdentifier,
238    name: String,
239    description: String,
240    status_flags: StatusFlags,
241    out_of_service: bool,
242    reliability: Reliability,
243    /// The group member references (device, object, property). Only
244    /// `set_group_members` changes them, so each Device member is a Device.
245    group_members: Vec<BACnetDeviceObjectPropertyReference>,
246    /// What the last read of each member produced, by position in
247    /// Group_Members (populated externally).
248    pub present_value: Vec<AccessResult>,
249    /// Human-readable names for each member.
250    pub group_member_names: Vec<String>,
251}
252
253impl GlobalGroupObject {
254    /// Create a new GlobalGroup object.
255    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
256        let oid = ObjectIdentifier::new(ObjectType::GLOBAL_GROUP, instance)?;
257        Ok(Self {
258            oid,
259            name: name.into(),
260            description: String::new(),
261            status_flags: StatusFlags::empty(),
262            out_of_service: false,
263            reliability: Reliability::NO_FAULT_DETECTED,
264            group_members: Vec::new(),
265            present_value: Vec::new(),
266            group_member_names: Vec::new(),
267        })
268    }
269
270    /// Replace Group_Members, the references whose values the group reports,
271    /// each naming an object in this device or, through its device
272    /// identifier, in another. The array is read-only over the network.
273    ///
274    /// A reference whose device identifier isn't a Device object is refused
275    /// with PROPERTY / VALUE_OUT_OF_RANGE and the members set before are kept
276    /// (#1308). `present_value` and `group_member_names` stay as they are:
277    /// they follow the members by position.
278    pub fn set_group_members(
279        &mut self,
280        members: Vec<BACnetDeviceObjectPropertyReference>,
281    ) -> Result<(), Error> {
282        device_reference::check_device_members(&members)?;
283        self.group_members = members;
284        Ok(())
285    }
286
287    /// Append one reference to Group_Members, refused like
288    /// [`set_group_members`](Self::set_group_members): a device identifier
289    /// that isn't a Device object is VALUE_OUT_OF_RANGE and adds nothing.
290    pub fn add_group_member(
291        &mut self,
292        member: BACnetDeviceObjectPropertyReference,
293    ) -> Result<(), Error> {
294        device_reference::check_device_member(member.device_identifier)?;
295        self.group_members.push(member);
296        Ok(())
297    }
298
299    /// The Group_Members references, in order.
300    pub fn group_members(&self) -> &[BACnetDeviceObjectPropertyReference] {
301        &self.group_members
302    }
303
304    /// Member_Status_Flags (Clause 12.50.10): the OR of every Status_Flags
305    /// value held in Present_Value.
306    ///
307    /// A member contributes when its reference names Status_Flags and its
308    /// stored result is a bit-string value. Other members, and members with
309    /// no stored result, an error result or a value of another type, add
310    /// nothing, so a group without Status_Flags members reads all-clear.
311    pub fn member_status_flags(&self) -> StatusFlags {
312        let status_flags = PropertyIdentifier::STATUS_FLAGS.to_raw();
313        self.group_members
314            .iter()
315            .zip(&self.present_value)
316            .filter(|(member, _)| member.property_identifier == status_flags)
317            .filter_map(|(_, result)| match result {
318                AccessResult::Value(PropertyValue::BitString { data, .. }) => {
319                    Some(status_flags_from_bacnet(data))
320                }
321                _ => None,
322            })
323            .fold(StatusFlags::empty(), |combined, flags| combined | flags)
324    }
325
326    /// The Present_Value elements, one encoded `BACnetPropertyAccessResult`
327    /// per member in Group_Members order.
328    fn present_value_elements(&self) -> Result<Vec<PropertyValue>, Error> {
329        self.group_members
330            .iter()
331            .enumerate()
332            .map(|(index, reference)| {
333                let element = BACnetPropertyAccessResult {
334                    reference: reference.clone(),
335                    access_result: self
336                        .present_value
337                        .get(index)
338                        .cloned()
339                        .unwrap_or(AccessResult::NOT_INITIALIZED),
340                };
341                let mut encoded = BytesMut::new();
342                encode_property_access_result(&mut encoded, &element)?;
343                Ok(PropertyValue::ApplicationData(encoded.to_vec()))
344            })
345            .collect()
346    }
347}
348
349impl BACnetObject for GlobalGroupObject {
350    fn object_identifier(&self) -> ObjectIdentifier {
351        self.oid
352    }
353
354    fn object_name(&self) -> &str {
355        &self.name
356    }
357
358    fn read_property(
359        &self,
360        property: PropertyIdentifier,
361        array_index: Option<u32>,
362    ) -> Result<PropertyValue, Error> {
363        if let Some(result) = read_common_properties!(self, property, array_index) {
364            return result;
365        }
366        match property {
367            p if p == PropertyIdentifier::OBJECT_TYPE => {
368                Ok(PropertyValue::Enumerated(ObjectType::GLOBAL_GROUP.to_raw()))
369            }
370            // Each array element is one Clause 21 production: a member
371            // reference for Group_Members, a reference and its access result
372            // for Present_Value (Table 12-57).
373            p if p == PropertyIdentifier::GROUP_MEMBERS => common::read_array(
374                device_reference::reference_elements(&self.group_members),
375                array_index,
376            ),
377            p if p == PropertyIdentifier::PRESENT_VALUE => {
378                common::read_array(self.present_value_elements()?, array_index)
379            }
380            p if p == PropertyIdentifier::GROUP_MEMBER_NAMES => common::read_array(
381                self.group_member_names
382                    .iter()
383                    .map(|n| PropertyValue::CharacterString(n.clone()))
384                    .collect(),
385                array_index,
386            ),
387            p if p == PropertyIdentifier::EVENT_STATE => {
388                Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
389            }
390            p if p == PropertyIdentifier::MEMBER_STATUS_FLAGS => Ok(PropertyValue::BitString {
391                unused_bits: 4,
392                data: vec![self.member_status_flags().bits() << 4],
393            }),
394            _ => Err(common::unknown_property_error()),
395        }
396    }
397
398    fn write_property(
399        &mut self,
400        property: PropertyIdentifier,
401        _array_index: Option<u32>,
402        value: PropertyValue,
403        _priority: Option<u8>,
404    ) -> Result<(), Error> {
405        if let Some(result) =
406            common::write_out_of_service(&mut self.out_of_service, property, &value)
407        {
408            return result;
409        }
410        if let Some(result) = common::write_description(&mut self.description, property, &value) {
411            return result;
412        }
413        Err(crate::common::unhandled_write_error(
414            self.property_metadata().as_ref(),
415            property,
416            _array_index,
417        ))
418    }
419
420    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
421        metadata::for_global_group_object(self)
422    }
423
424    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
425        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
426    }
427}
428
429// ---------------------------------------------------------------------------
430// StructuredViewObject (type 29)
431// ---------------------------------------------------------------------------
432
433/// BACnet StructuredView object (type 29).
434///
435/// Provides a hierarchical view of BACnet objects. NODE_TYPE classifies
436/// the node role, SUBORDINATE_LIST holds child object references, and
437/// SUBORDINATE_ANNOTATIONS provides per-child descriptions. Both are arrays
438/// (Table 12-34): index 0 reads the size and each index from 1 one element.
439pub struct StructuredViewObject {
440    oid: ObjectIdentifier,
441    name: String,
442    description: String,
443    /// Node type enumeration value (per BACnetNodeType).
444    pub node_type: u32,
445    /// Node subtype — optional character string.
446    pub node_subtype: String,
447    /// Child object references, served as BACnetDeviceObjectReference
448    /// elements; one without a device names an object in this device. Only
449    /// `add_subordinate` adds to it, so each device member is a Device.
450    subordinate_list: Vec<BACnetDeviceObjectReference>,
451    /// Per-child annotations (parallel to subordinate_list).
452    subordinate_annotations: Vec<String>,
453}
454
455impl StructuredViewObject {
456    /// Create a new StructuredView object.
457    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
458        let oid = ObjectIdentifier::new(ObjectType::STRUCTURED_VIEW, instance)?;
459        Ok(Self {
460            oid,
461            name: name.into(),
462            description: String::new(),
463            node_type: 0,
464            node_subtype: String::new(),
465            subordinate_list: Vec::new(),
466            subordinate_annotations: Vec::new(),
467        })
468    }
469
470    /// Add a subordinate with an annotation. An [`ObjectIdentifier`] alone
471    /// names an object in this device.
472    ///
473    /// A reference whose device identifier isn't a Device object is refused
474    /// with VALUE_OUT_OF_RANGE and neither array changes (#1285).
475    pub fn add_subordinate(
476        &mut self,
477        reference: impl Into<BACnetDeviceObjectReference>,
478        annotation: impl Into<String>,
479    ) -> Result<(), Error> {
480        let reference = reference.into();
481        device_reference::check_device_member(reference.device_identifier)?;
482        self.subordinate_list.push(reference);
483        self.subordinate_annotations.push(annotation.into());
484        Ok(())
485    }
486
487    /// Replace every subordinate, each a reference with its annotation, so
488    /// Subordinate_List and Subordinate_Annotations keep one size. An empty
489    /// list clears both.
490    ///
491    /// A reference whose device identifier isn't a Device object is refused
492    /// with VALUE_OUT_OF_RANGE and neither array changes (#1285).
493    pub fn set_subordinates(
494        &mut self,
495        subordinates: Vec<(BACnetDeviceObjectReference, String)>,
496    ) -> Result<(), Error> {
497        for (reference, _) in &subordinates {
498            device_reference::check_device_member(reference.device_identifier)?;
499        }
500        (self.subordinate_list, self.subordinate_annotations) = subordinates.into_iter().unzip();
501        Ok(())
502    }
503
504    /// The subordinates in order, each reference with its annotation.
505    pub fn subordinates(&self) -> impl Iterator<Item = (&BACnetDeviceObjectReference, &str)> {
506        self.subordinate_list
507            .iter()
508            .zip(self.subordinate_annotations.iter().map(String::as_str))
509    }
510}
511
512impl BACnetObject for StructuredViewObject {
513    fn object_identifier(&self) -> ObjectIdentifier {
514        self.oid
515    }
516
517    fn object_name(&self) -> &str {
518        &self.name
519    }
520
521    fn read_property(
522        &self,
523        property: PropertyIdentifier,
524        array_index: Option<u32>,
525    ) -> Result<PropertyValue, Error> {
526        // Table 12-34 has no Status_Flags, Reliability or Out_Of_Service (#1064).
527        if let Some(result) = read_identity_properties!(self, property, array_index) {
528            return result;
529        }
530        match property {
531            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
532                ObjectType::STRUCTURED_VIEW.to_raw(),
533            )),
534            p if p == PropertyIdentifier::NODE_TYPE => {
535                Ok(PropertyValue::Enumerated(self.node_type))
536            }
537            p if p == PropertyIdentifier::NODE_SUBTYPE => {
538                Ok(PropertyValue::CharacterString(self.node_subtype.clone()))
539            }
540            p if p == PropertyIdentifier::SUBORDINATE_LIST => common::read_array(
541                device_reference::reference_elements(&self.subordinate_list),
542                array_index,
543            ),
544            p if p == PropertyIdentifier::SUBORDINATE_ANNOTATIONS => common::read_array(
545                self.subordinate_annotations
546                    .iter()
547                    .cloned()
548                    .map(PropertyValue::CharacterString)
549                    .collect(),
550                array_index,
551            ),
552            _ => Err(common::unknown_property_error()),
553        }
554    }
555
556    fn write_property(
557        &mut self,
558        property: PropertyIdentifier,
559        _array_index: Option<u32>,
560        value: PropertyValue,
561        _priority: Option<u8>,
562    ) -> Result<(), Error> {
563        if let Some(result) = common::write_description(&mut self.description, property, &value) {
564            return result;
565        }
566        Err(crate::common::unhandled_write_error(
567            self.property_metadata().as_ref(),
568            property,
569            _array_index,
570        ))
571    }
572
573    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
574        metadata::for_structured_view_object(self)
575    }
576
577    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
578        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
579    }
580}
581
582// ===========================================================================
583// Tests
584// ===========================================================================
585
586#[cfg(test)]
587mod array_tests;
588
589#[cfg(test)]
590mod group_members_tests;
591
592#[cfg(test)]
593mod member_status_flags_tests;
594
595#[cfg(test)]
596mod tests;