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