Skip to main content

bacnet_objects/
staging.rs

1//! Staging object (type 60) per ASHRAE 135-2020 Clause 12.
2//!
3//! The Staging object organizes a set of stages (numbered 0..N-1) with
4//! human-readable names and target references. Present value indicates
5//! the current active stage.
6
7use bacnet_types::enums::{ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10use std::borrow::Cow;
11
12use crate::common::{self, read_common_properties};
13use crate::traits::BACnetObject;
14
15/// BACnet Staging object — manages a set of named stages.
16pub struct StagingObject {
17    oid: ObjectIdentifier,
18    name: String,
19    description: String,
20    present_value: u64,
21    stage_names: Vec<String>,
22    target_references: Vec<Vec<u8>>,
23    status_flags: StatusFlags,
24    out_of_service: bool,
25    reliability: u32,
26}
27
28impl StagingObject {
29    /// Create a new Staging object with the given number of stages.
30    ///
31    /// Stage names default to "Stage 0", "Stage 1", etc.
32    pub fn new(instance: u32, name: impl Into<String>, num_stages: usize) -> Result<Self, Error> {
33        let oid = ObjectIdentifier::new(ObjectType::STAGING, instance)?;
34        let stage_names = (0..num_stages).map(|i| format!("Stage {i}")).collect();
35        let target_references = vec![Vec::new(); num_stages];
36        Ok(Self {
37            oid,
38            name: name.into(),
39            description: String::new(),
40            present_value: 0,
41            stage_names,
42            target_references,
43            status_flags: StatusFlags::empty(),
44            out_of_service: false,
45            reliability: 0,
46        })
47    }
48
49    /// Set a stage name by index.
50    pub fn set_stage_name(&mut self, index: usize, name: impl Into<String>) {
51        if index < self.stage_names.len() {
52            self.stage_names[index] = name.into();
53        }
54    }
55}
56
57impl BACnetObject for StagingObject {
58    fn object_identifier(&self) -> ObjectIdentifier {
59        self.oid
60    }
61
62    fn object_name(&self) -> &str {
63        &self.name
64    }
65
66    fn read_property(
67        &self,
68        property: PropertyIdentifier,
69        array_index: Option<u32>,
70    ) -> Result<PropertyValue, Error> {
71        if let Some(result) = read_common_properties!(self, property, array_index) {
72            return result;
73        }
74        match property {
75            p if p == PropertyIdentifier::OBJECT_TYPE => {
76                Ok(PropertyValue::Enumerated(ObjectType::STAGING.to_raw()))
77            }
78            p if p == PropertyIdentifier::PRESENT_VALUE => {
79                Ok(PropertyValue::Unsigned(self.present_value))
80            }
81            p if p == PropertyIdentifier::STAGE_NAMES => {
82                let items: Vec<PropertyValue> = self
83                    .stage_names
84                    .iter()
85                    .map(|s| PropertyValue::CharacterString(s.clone()))
86                    .collect();
87                Ok(PropertyValue::List(items))
88            }
89            p if p == PropertyIdentifier::TARGET_REFERENCES => {
90                let items: Vec<PropertyValue> = self
91                    .target_references
92                    .iter()
93                    .map(|r| PropertyValue::OctetString(r.clone()))
94                    .collect();
95                Ok(PropertyValue::List(items))
96            }
97            _ => Err(common::unknown_property_error()),
98        }
99    }
100
101    fn write_property(
102        &mut self,
103        property: PropertyIdentifier,
104        _array_index: Option<u32>,
105        value: PropertyValue,
106        _priority: Option<u8>,
107    ) -> Result<(), Error> {
108        if let Some(result) =
109            common::write_out_of_service(&mut self.out_of_service, property, &value)
110        {
111            return result;
112        }
113        if let Some(result) = common::write_description(&mut self.description, property, &value) {
114            return result;
115        }
116        match property {
117            p if p == PropertyIdentifier::PRESENT_VALUE => {
118                if let PropertyValue::Unsigned(v) = value {
119                    if v as usize >= self.stage_names.len() {
120                        return Err(common::value_out_of_range_error());
121                    }
122                    self.present_value = v;
123                    Ok(())
124                } else {
125                    Err(common::invalid_data_type_error())
126                }
127            }
128            _ => Err(common::write_access_denied_error()),
129        }
130    }
131
132    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
133        static PROPS: &[PropertyIdentifier] = &[
134            PropertyIdentifier::OBJECT_IDENTIFIER,
135            PropertyIdentifier::OBJECT_NAME,
136            PropertyIdentifier::DESCRIPTION,
137            PropertyIdentifier::OBJECT_TYPE,
138            PropertyIdentifier::PRESENT_VALUE,
139            PropertyIdentifier::STAGE_NAMES,
140            PropertyIdentifier::TARGET_REFERENCES,
141            PropertyIdentifier::STATUS_FLAGS,
142            PropertyIdentifier::OUT_OF_SERVICE,
143            PropertyIdentifier::RELIABILITY,
144        ];
145        Cow::Borrowed(PROPS)
146    }
147}
148
149// ---------------------------------------------------------------------------
150// Tests
151// ---------------------------------------------------------------------------
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn staging_create_and_read_defaults() {
159        let stg = StagingObject::new(1, "STG-1", 3).unwrap();
160        assert_eq!(stg.object_name(), "STG-1");
161        assert_eq!(
162            stg.read_property(PropertyIdentifier::PRESENT_VALUE, None)
163                .unwrap(),
164            PropertyValue::Unsigned(0)
165        );
166    }
167
168    #[test]
169    fn staging_object_type() {
170        let stg = StagingObject::new(1, "STG-1", 3).unwrap();
171        assert_eq!(
172            stg.read_property(PropertyIdentifier::OBJECT_TYPE, None)
173                .unwrap(),
174            PropertyValue::Enumerated(ObjectType::STAGING.to_raw())
175        );
176    }
177
178    #[test]
179    fn staging_read_stage_names() {
180        let stg = StagingObject::new(1, "STG-1", 3).unwrap();
181        let val = stg
182            .read_property(PropertyIdentifier::STAGE_NAMES, None)
183            .unwrap();
184        assert_eq!(
185            val,
186            PropertyValue::List(vec![
187                PropertyValue::CharacterString("Stage 0".into()),
188                PropertyValue::CharacterString("Stage 1".into()),
189                PropertyValue::CharacterString("Stage 2".into()),
190            ])
191        );
192    }
193
194    #[test]
195    fn staging_set_stage_name() {
196        let mut stg = StagingObject::new(1, "STG-1", 2).unwrap();
197        stg.set_stage_name(0, "Off");
198        stg.set_stage_name(1, "On");
199        let val = stg
200            .read_property(PropertyIdentifier::STAGE_NAMES, None)
201            .unwrap();
202        assert_eq!(
203            val,
204            PropertyValue::List(vec![
205                PropertyValue::CharacterString("Off".into()),
206                PropertyValue::CharacterString("On".into()),
207            ])
208        );
209    }
210
211    #[test]
212    fn staging_write_present_value() {
213        let mut stg = StagingObject::new(1, "STG-1", 3).unwrap();
214        stg.write_property(
215            PropertyIdentifier::PRESENT_VALUE,
216            None,
217            PropertyValue::Unsigned(2),
218            None,
219        )
220        .unwrap();
221        assert_eq!(
222            stg.read_property(PropertyIdentifier::PRESENT_VALUE, None)
223                .unwrap(),
224            PropertyValue::Unsigned(2)
225        );
226    }
227
228    #[test]
229    fn staging_write_present_value_out_of_range() {
230        let mut stg = StagingObject::new(1, "STG-1", 3).unwrap();
231        let result = stg.write_property(
232            PropertyIdentifier::PRESENT_VALUE,
233            None,
234            PropertyValue::Unsigned(5),
235            None,
236        );
237        assert!(result.is_err());
238    }
239
240    #[test]
241    fn staging_write_present_value_wrong_type() {
242        let mut stg = StagingObject::new(1, "STG-1", 3).unwrap();
243        let result = stg.write_property(
244            PropertyIdentifier::PRESENT_VALUE,
245            None,
246            PropertyValue::Real(1.0),
247            None,
248        );
249        assert!(result.is_err());
250    }
251
252    #[test]
253    fn staging_read_target_references() {
254        let stg = StagingObject::new(1, "STG-1", 2).unwrap();
255        let val = stg
256            .read_property(PropertyIdentifier::TARGET_REFERENCES, None)
257            .unwrap();
258        assert_eq!(
259            val,
260            PropertyValue::List(vec![
261                PropertyValue::OctetString(vec![]),
262                PropertyValue::OctetString(vec![]),
263            ])
264        );
265    }
266
267    #[test]
268    fn staging_property_list() {
269        let stg = StagingObject::new(1, "STG-1", 3).unwrap();
270        let list = stg.property_list();
271        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
272        assert!(list.contains(&PropertyIdentifier::STAGE_NAMES));
273        assert!(list.contains(&PropertyIdentifier::TARGET_REFERENCES));
274    }
275}