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.24
5//! - StructuredViewObject (type 29) — Clause 12.29
6
7use bacnet_types::constructed::BACnetDeviceObjectPropertyReference;
8use bacnet_types::enums::{ObjectType, PropertyIdentifier};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
11use std::borrow::Cow;
12
13use crate::common::{self, read_common_properties};
14use crate::traits::BACnetObject;
15
16// ---------------------------------------------------------------------------
17// GroupObject (type 11)
18// ---------------------------------------------------------------------------
19
20/// BACnet Group object (type 11).
21///
22/// Groups a set of BACnet objects together. The LIST_OF_GROUP_MEMBERS contains
23/// the ObjectIdentifiers of the member objects. PRESENT_VALUE returns the last
24/// read results (empty by default).
25pub struct GroupObject {
26    oid: ObjectIdentifier,
27    name: String,
28    description: String,
29    status_flags: StatusFlags,
30    out_of_service: bool,
31    reliability: u32,
32    /// The list of group member object identifiers.
33    pub list_of_group_members: Vec<ObjectIdentifier>,
34    /// The last read results for each member (populated externally).
35    pub present_value: Vec<PropertyValue>,
36}
37
38impl GroupObject {
39    /// Create a new Group object.
40    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
41        let oid = ObjectIdentifier::new(ObjectType::GROUP, instance)?;
42        Ok(Self {
43            oid,
44            name: name.into(),
45            description: String::new(),
46            status_flags: StatusFlags::empty(),
47            out_of_service: false,
48            reliability: 0,
49            list_of_group_members: Vec::new(),
50            present_value: Vec::new(),
51        })
52    }
53
54    /// Add a member object to the group.
55    pub fn add_member(&mut self, oid: ObjectIdentifier) {
56        self.list_of_group_members.push(oid);
57    }
58
59    /// Clear all members from the group.
60    pub fn clear_members(&mut self) {
61        self.list_of_group_members.clear();
62        self.present_value.clear();
63    }
64}
65
66impl BACnetObject for GroupObject {
67    fn object_identifier(&self) -> ObjectIdentifier {
68        self.oid
69    }
70
71    fn object_name(&self) -> &str {
72        &self.name
73    }
74
75    fn read_property(
76        &self,
77        property: PropertyIdentifier,
78        array_index: Option<u32>,
79    ) -> Result<PropertyValue, Error> {
80        if let Some(result) = read_common_properties!(self, property, array_index) {
81            return result;
82        }
83        match property {
84            p if p == PropertyIdentifier::OBJECT_TYPE => {
85                Ok(PropertyValue::Enumerated(ObjectType::GROUP.to_raw()))
86            }
87            p if p == PropertyIdentifier::LIST_OF_GROUP_MEMBERS => Ok(PropertyValue::List(
88                self.list_of_group_members
89                    .iter()
90                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
91                    .collect(),
92            )),
93            p if p == PropertyIdentifier::PRESENT_VALUE => {
94                Ok(PropertyValue::List(self.present_value.clone()))
95            }
96            _ => Err(common::unknown_property_error()),
97        }
98    }
99
100    fn write_property(
101        &mut self,
102        property: PropertyIdentifier,
103        _array_index: Option<u32>,
104        value: PropertyValue,
105        _priority: Option<u8>,
106    ) -> Result<(), Error> {
107        if let Some(result) =
108            common::write_out_of_service(&mut self.out_of_service, property, &value)
109        {
110            return result;
111        }
112        if let Some(result) = common::write_description(&mut self.description, property, &value) {
113            return result;
114        }
115        Err(common::write_access_denied_error())
116    }
117
118    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
119        static PROPS: &[PropertyIdentifier] = &[
120            PropertyIdentifier::OBJECT_IDENTIFIER,
121            PropertyIdentifier::OBJECT_NAME,
122            PropertyIdentifier::DESCRIPTION,
123            PropertyIdentifier::OBJECT_TYPE,
124            PropertyIdentifier::LIST_OF_GROUP_MEMBERS,
125            PropertyIdentifier::PRESENT_VALUE,
126            PropertyIdentifier::STATUS_FLAGS,
127            PropertyIdentifier::OUT_OF_SERVICE,
128            PropertyIdentifier::RELIABILITY,
129        ];
130        Cow::Borrowed(PROPS)
131    }
132}
133
134// ---------------------------------------------------------------------------
135// GlobalGroupObject (type 26)
136// ---------------------------------------------------------------------------
137
138/// BACnet GlobalGroup object (type 26).
139///
140/// Similar to Group but members are DeviceObjectPropertyReference entries,
141/// allowing references to properties on remote devices. GROUP_MEMBER_NAMES
142/// provides human-readable names for each member.
143pub struct GlobalGroupObject {
144    oid: ObjectIdentifier,
145    name: String,
146    description: String,
147    status_flags: StatusFlags,
148    out_of_service: bool,
149    reliability: u32,
150    /// The group member references (device, object, property).
151    pub group_members: Vec<BACnetDeviceObjectPropertyReference>,
152    /// The last read results for each member (populated externally).
153    pub present_value: Vec<PropertyValue>,
154    /// Human-readable names for each member.
155    pub group_member_names: Vec<String>,
156}
157
158impl GlobalGroupObject {
159    /// Create a new GlobalGroup object.
160    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
161        let oid = ObjectIdentifier::new(ObjectType::GLOBAL_GROUP, instance)?;
162        Ok(Self {
163            oid,
164            name: name.into(),
165            description: String::new(),
166            status_flags: StatusFlags::empty(),
167            out_of_service: false,
168            reliability: 0,
169            group_members: Vec::new(),
170            present_value: Vec::new(),
171            group_member_names: Vec::new(),
172        })
173    }
174}
175
176impl BACnetObject for GlobalGroupObject {
177    fn object_identifier(&self) -> ObjectIdentifier {
178        self.oid
179    }
180
181    fn object_name(&self) -> &str {
182        &self.name
183    }
184
185    fn read_property(
186        &self,
187        property: PropertyIdentifier,
188        array_index: Option<u32>,
189    ) -> Result<PropertyValue, Error> {
190        if let Some(result) = read_common_properties!(self, property, array_index) {
191            return result;
192        }
193        match property {
194            p if p == PropertyIdentifier::OBJECT_TYPE => {
195                Ok(PropertyValue::Enumerated(ObjectType::GLOBAL_GROUP.to_raw()))
196            }
197            p if p == PropertyIdentifier::GROUP_MEMBERS => Ok(PropertyValue::List(
198                self.group_members
199                    .iter()
200                    .map(|r| {
201                        PropertyValue::List(vec![
202                            PropertyValue::ObjectIdentifier(r.object_identifier),
203                            PropertyValue::Unsigned(r.property_identifier as u64),
204                            match r.property_array_index {
205                                Some(idx) => PropertyValue::Unsigned(idx as u64),
206                                None => PropertyValue::Null,
207                            },
208                            match r.device_identifier {
209                                Some(dev) => PropertyValue::ObjectIdentifier(dev),
210                                None => PropertyValue::Null,
211                            },
212                        ])
213                    })
214                    .collect(),
215            )),
216            p if p == PropertyIdentifier::PRESENT_VALUE => {
217                Ok(PropertyValue::List(self.present_value.clone()))
218            }
219            p if p == PropertyIdentifier::GROUP_MEMBER_NAMES => Ok(PropertyValue::List(
220                self.group_member_names
221                    .iter()
222                    .map(|n| PropertyValue::CharacterString(n.clone()))
223                    .collect(),
224            )),
225            _ => Err(common::unknown_property_error()),
226        }
227    }
228
229    fn write_property(
230        &mut self,
231        property: PropertyIdentifier,
232        _array_index: Option<u32>,
233        value: PropertyValue,
234        _priority: Option<u8>,
235    ) -> Result<(), Error> {
236        if let Some(result) =
237            common::write_out_of_service(&mut self.out_of_service, property, &value)
238        {
239            return result;
240        }
241        if let Some(result) = common::write_description(&mut self.description, property, &value) {
242            return result;
243        }
244        Err(common::write_access_denied_error())
245    }
246
247    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
248        static PROPS: &[PropertyIdentifier] = &[
249            PropertyIdentifier::OBJECT_IDENTIFIER,
250            PropertyIdentifier::OBJECT_NAME,
251            PropertyIdentifier::DESCRIPTION,
252            PropertyIdentifier::OBJECT_TYPE,
253            PropertyIdentifier::GROUP_MEMBERS,
254            PropertyIdentifier::PRESENT_VALUE,
255            PropertyIdentifier::GROUP_MEMBER_NAMES,
256            PropertyIdentifier::STATUS_FLAGS,
257            PropertyIdentifier::OUT_OF_SERVICE,
258            PropertyIdentifier::RELIABILITY,
259        ];
260        Cow::Borrowed(PROPS)
261    }
262}
263
264// ---------------------------------------------------------------------------
265// StructuredViewObject (type 29)
266// ---------------------------------------------------------------------------
267
268/// BACnet StructuredView object (type 29).
269///
270/// Provides a hierarchical view of BACnet objects. NODE_TYPE classifies
271/// the node role, SUBORDINATE_LIST holds child object references, and
272/// SUBORDINATE_ANNOTATIONS provides per-child descriptions.
273pub struct StructuredViewObject {
274    oid: ObjectIdentifier,
275    name: String,
276    description: String,
277    status_flags: StatusFlags,
278    out_of_service: bool,
279    reliability: u32,
280    /// Node type enumeration value (per BACnetNodeType).
281    pub node_type: u32,
282    /// Node subtype — optional character string.
283    pub node_subtype: String,
284    /// Child object identifiers.
285    pub subordinate_list: Vec<ObjectIdentifier>,
286    /// Per-child annotations (parallel to subordinate_list).
287    pub subordinate_annotations: Vec<String>,
288}
289
290impl StructuredViewObject {
291    /// Create a new StructuredView object.
292    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
293        let oid = ObjectIdentifier::new(ObjectType::STRUCTURED_VIEW, instance)?;
294        Ok(Self {
295            oid,
296            name: name.into(),
297            description: String::new(),
298            status_flags: StatusFlags::empty(),
299            out_of_service: false,
300            reliability: 0,
301            node_type: 0,
302            node_subtype: String::new(),
303            subordinate_list: Vec::new(),
304            subordinate_annotations: Vec::new(),
305        })
306    }
307
308    /// Add a subordinate object with an annotation.
309    pub fn add_subordinate(&mut self, oid: ObjectIdentifier, annotation: impl Into<String>) {
310        self.subordinate_list.push(oid);
311        self.subordinate_annotations.push(annotation.into());
312    }
313}
314
315impl BACnetObject for StructuredViewObject {
316    fn object_identifier(&self) -> ObjectIdentifier {
317        self.oid
318    }
319
320    fn object_name(&self) -> &str {
321        &self.name
322    }
323
324    fn read_property(
325        &self,
326        property: PropertyIdentifier,
327        array_index: Option<u32>,
328    ) -> Result<PropertyValue, Error> {
329        if let Some(result) = read_common_properties!(self, property, array_index) {
330            return result;
331        }
332        match property {
333            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
334                ObjectType::STRUCTURED_VIEW.to_raw(),
335            )),
336            p if p == PropertyIdentifier::NODE_TYPE => {
337                Ok(PropertyValue::Enumerated(self.node_type))
338            }
339            p if p == PropertyIdentifier::NODE_SUBTYPE => {
340                Ok(PropertyValue::CharacterString(self.node_subtype.clone()))
341            }
342            p if p == PropertyIdentifier::SUBORDINATE_LIST => Ok(PropertyValue::List(
343                self.subordinate_list
344                    .iter()
345                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
346                    .collect(),
347            )),
348            p if p == PropertyIdentifier::SUBORDINATE_ANNOTATIONS => Ok(PropertyValue::List(
349                self.subordinate_annotations
350                    .iter()
351                    .map(|a| PropertyValue::CharacterString(a.clone()))
352                    .collect(),
353            )),
354            _ => Err(common::unknown_property_error()),
355        }
356    }
357
358    fn write_property(
359        &mut self,
360        property: PropertyIdentifier,
361        _array_index: Option<u32>,
362        value: PropertyValue,
363        _priority: Option<u8>,
364    ) -> Result<(), Error> {
365        if let Some(result) =
366            common::write_out_of_service(&mut self.out_of_service, property, &value)
367        {
368            return result;
369        }
370        if let Some(result) = common::write_description(&mut self.description, property, &value) {
371            return result;
372        }
373        Err(common::write_access_denied_error())
374    }
375
376    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
377        static PROPS: &[PropertyIdentifier] = &[
378            PropertyIdentifier::OBJECT_IDENTIFIER,
379            PropertyIdentifier::OBJECT_NAME,
380            PropertyIdentifier::DESCRIPTION,
381            PropertyIdentifier::OBJECT_TYPE,
382            PropertyIdentifier::NODE_TYPE,
383            PropertyIdentifier::NODE_SUBTYPE,
384            PropertyIdentifier::SUBORDINATE_LIST,
385            PropertyIdentifier::SUBORDINATE_ANNOTATIONS,
386            PropertyIdentifier::STATUS_FLAGS,
387            PropertyIdentifier::OUT_OF_SERVICE,
388            PropertyIdentifier::RELIABILITY,
389        ];
390        Cow::Borrowed(PROPS)
391    }
392}
393
394// ===========================================================================
395// Tests
396// ===========================================================================
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    // -----------------------------------------------------------------------
403    // GroupObject tests
404    // -----------------------------------------------------------------------
405
406    #[test]
407    fn group_create() {
408        let g = GroupObject::new(1, "Group-1").unwrap();
409        assert_eq!(g.object_identifier().object_type(), ObjectType::GROUP);
410        assert_eq!(g.object_identifier().instance_number(), 1);
411        assert_eq!(g.object_name(), "Group-1");
412    }
413
414    #[test]
415    fn group_object_type() {
416        let g = GroupObject::new(1, "G").unwrap();
417        let val = g
418            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
419            .unwrap();
420        assert_eq!(val, PropertyValue::Enumerated(ObjectType::GROUP.to_raw()));
421    }
422
423    #[test]
424    fn group_add_members() {
425        let mut g = GroupObject::new(1, "G").unwrap();
426        let ai1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
427        let ai2 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap();
428        g.add_member(ai1);
429        g.add_member(ai2);
430
431        let val = g
432            .read_property(PropertyIdentifier::LIST_OF_GROUP_MEMBERS, None)
433            .unwrap();
434        if let PropertyValue::List(items) = val {
435            assert_eq!(items.len(), 2);
436            assert_eq!(items[0], PropertyValue::ObjectIdentifier(ai1));
437            assert_eq!(items[1], PropertyValue::ObjectIdentifier(ai2));
438        } else {
439            panic!("Expected List");
440        }
441    }
442
443    #[test]
444    fn group_clear_members() {
445        let mut g = GroupObject::new(1, "G").unwrap();
446        let ai1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
447        g.add_member(ai1);
448        assert_eq!(g.list_of_group_members.len(), 1);
449        g.clear_members();
450        assert!(g.list_of_group_members.is_empty());
451    }
452
453    #[test]
454    fn group_present_value_empty() {
455        let g = GroupObject::new(1, "G").unwrap();
456        let val = g
457            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
458            .unwrap();
459        if let PropertyValue::List(items) = val {
460            assert!(items.is_empty());
461        } else {
462            panic!("Expected List");
463        }
464    }
465
466    #[test]
467    fn group_property_list() {
468        let g = GroupObject::new(1, "G").unwrap();
469        let props = g.property_list();
470        assert!(props.contains(&PropertyIdentifier::LIST_OF_GROUP_MEMBERS));
471        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
472        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
473    }
474
475    // -----------------------------------------------------------------------
476    // GlobalGroupObject tests
477    // -----------------------------------------------------------------------
478
479    #[test]
480    fn global_group_create() {
481        let gg = GlobalGroupObject::new(1, "GG-1").unwrap();
482        assert_eq!(
483            gg.object_identifier().object_type(),
484            ObjectType::GLOBAL_GROUP
485        );
486        assert_eq!(gg.object_identifier().instance_number(), 1);
487        assert_eq!(gg.object_name(), "GG-1");
488    }
489
490    #[test]
491    fn global_group_object_type() {
492        let gg = GlobalGroupObject::new(1, "GG").unwrap();
493        let val = gg
494            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
495            .unwrap();
496        assert_eq!(
497            val,
498            PropertyValue::Enumerated(ObjectType::GLOBAL_GROUP.to_raw())
499        );
500    }
501
502    #[test]
503    fn global_group_members_empty() {
504        let gg = GlobalGroupObject::new(1, "GG").unwrap();
505        let val = gg
506            .read_property(PropertyIdentifier::GROUP_MEMBERS, None)
507            .unwrap();
508        if let PropertyValue::List(items) = val {
509            assert!(items.is_empty());
510        } else {
511            panic!("Expected List");
512        }
513    }
514
515    #[test]
516    fn global_group_member_names() {
517        let mut gg = GlobalGroupObject::new(1, "GG").unwrap();
518        gg.group_member_names.push("Temp Sensor".into());
519        gg.group_member_names.push("Humidity".into());
520
521        let val = gg
522            .read_property(PropertyIdentifier::GROUP_MEMBER_NAMES, None)
523            .unwrap();
524        if let PropertyValue::List(items) = val {
525            assert_eq!(items.len(), 2);
526            assert_eq!(
527                items[0],
528                PropertyValue::CharacterString("Temp Sensor".into())
529            );
530            assert_eq!(items[1], PropertyValue::CharacterString("Humidity".into()));
531        } else {
532            panic!("Expected List");
533        }
534    }
535
536    #[test]
537    fn global_group_property_list() {
538        let gg = GlobalGroupObject::new(1, "GG").unwrap();
539        let props = gg.property_list();
540        assert!(props.contains(&PropertyIdentifier::GROUP_MEMBERS));
541        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
542        assert!(props.contains(&PropertyIdentifier::GROUP_MEMBER_NAMES));
543    }
544
545    // -----------------------------------------------------------------------
546    // StructuredViewObject tests
547    // -----------------------------------------------------------------------
548
549    #[test]
550    fn structured_view_create() {
551        let sv = StructuredViewObject::new(1, "SV-1").unwrap();
552        assert_eq!(
553            sv.object_identifier().object_type(),
554            ObjectType::STRUCTURED_VIEW
555        );
556        assert_eq!(sv.object_identifier().instance_number(), 1);
557        assert_eq!(sv.object_name(), "SV-1");
558    }
559
560    #[test]
561    fn structured_view_object_type() {
562        let sv = StructuredViewObject::new(1, "SV").unwrap();
563        let val = sv
564            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
565            .unwrap();
566        assert_eq!(
567            val,
568            PropertyValue::Enumerated(ObjectType::STRUCTURED_VIEW.to_raw())
569        );
570    }
571
572    #[test]
573    fn structured_view_add_subordinates() {
574        let mut sv = StructuredViewObject::new(1, "SV").unwrap();
575        let ai1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
576        let bi1 = ObjectIdentifier::new(ObjectType::BINARY_INPUT, 1).unwrap();
577        sv.add_subordinate(ai1, "Temperature");
578        sv.add_subordinate(bi1, "Occupancy");
579
580        let val = sv
581            .read_property(PropertyIdentifier::SUBORDINATE_LIST, None)
582            .unwrap();
583        if let PropertyValue::List(items) = val {
584            assert_eq!(items.len(), 2);
585            assert_eq!(items[0], PropertyValue::ObjectIdentifier(ai1));
586            assert_eq!(items[1], PropertyValue::ObjectIdentifier(bi1));
587        } else {
588            panic!("Expected List");
589        }
590
591        let ann = sv
592            .read_property(PropertyIdentifier::SUBORDINATE_ANNOTATIONS, None)
593            .unwrap();
594        if let PropertyValue::List(items) = ann {
595            assert_eq!(items.len(), 2);
596            assert_eq!(
597                items[0],
598                PropertyValue::CharacterString("Temperature".into())
599            );
600            assert_eq!(items[1], PropertyValue::CharacterString("Occupancy".into()));
601        } else {
602            panic!("Expected List");
603        }
604    }
605
606    #[test]
607    fn structured_view_node_type() {
608        let sv = StructuredViewObject::new(1, "SV").unwrap();
609        let val = sv
610            .read_property(PropertyIdentifier::NODE_TYPE, None)
611            .unwrap();
612        assert_eq!(val, PropertyValue::Enumerated(0));
613    }
614
615    #[test]
616    fn structured_view_node_subtype() {
617        let sv = StructuredViewObject::new(1, "SV").unwrap();
618        let val = sv
619            .read_property(PropertyIdentifier::NODE_SUBTYPE, None)
620            .unwrap();
621        assert_eq!(val, PropertyValue::CharacterString(String::new()));
622    }
623
624    #[test]
625    fn structured_view_property_list() {
626        let sv = StructuredViewObject::new(1, "SV").unwrap();
627        let props = sv.property_list();
628        assert!(props.contains(&PropertyIdentifier::NODE_TYPE));
629        assert!(props.contains(&PropertyIdentifier::NODE_SUBTYPE));
630        assert!(props.contains(&PropertyIdentifier::SUBORDINATE_LIST));
631        assert!(props.contains(&PropertyIdentifier::SUBORDINATE_ANNOTATIONS));
632    }
633}