Skip to main content

bacnet_objects/
command.rs

1//! Command object (type 7) per ASHRAE 135-2020 Clause 12.
2//!
3//! The Command object triggers a set of actions when its present value
4//! is written. Actions are stored as opaque byte vectors.
5
6use bacnet_types::enums::{ObjectType, PropertyIdentifier};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
9use std::borrow::Cow;
10
11use crate::common::{self, read_common_properties};
12use crate::traits::BACnetObject;
13
14/// BACnet Command object — triggers a set of actions on PV write.
15pub struct CommandObject {
16    oid: ObjectIdentifier,
17    name: String,
18    description: String,
19    present_value: u64,
20    in_process: bool,
21    all_writes_successful: bool,
22    action: Vec<Vec<u8>>,
23    status_flags: StatusFlags,
24    out_of_service: bool,
25    reliability: u32,
26}
27
28impl CommandObject {
29    /// Create a new Command object with default values.
30    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
31        let oid = ObjectIdentifier::new(ObjectType::COMMAND, instance)?;
32        Ok(Self {
33            oid,
34            name: name.into(),
35            description: String::new(),
36            present_value: 0,
37            in_process: false,
38            all_writes_successful: true,
39            action: Vec::new(),
40            status_flags: StatusFlags::empty(),
41            out_of_service: false,
42            reliability: 0,
43        })
44    }
45
46    /// Set the action list (opaque byte sequences).
47    pub fn set_action(&mut self, action: Vec<Vec<u8>>) {
48        self.action = action;
49    }
50}
51
52impl BACnetObject for CommandObject {
53    fn object_identifier(&self) -> ObjectIdentifier {
54        self.oid
55    }
56
57    fn object_name(&self) -> &str {
58        &self.name
59    }
60
61    fn read_property(
62        &self,
63        property: PropertyIdentifier,
64        array_index: Option<u32>,
65    ) -> Result<PropertyValue, Error> {
66        if let Some(result) = read_common_properties!(self, property, array_index) {
67            return result;
68        }
69        match property {
70            p if p == PropertyIdentifier::OBJECT_TYPE => {
71                Ok(PropertyValue::Enumerated(ObjectType::COMMAND.to_raw()))
72            }
73            p if p == PropertyIdentifier::PRESENT_VALUE => {
74                Ok(PropertyValue::Unsigned(self.present_value))
75            }
76            p if p == PropertyIdentifier::IN_PROCESS => Ok(PropertyValue::Boolean(self.in_process)),
77            p if p == PropertyIdentifier::ALL_WRITES_SUCCESSFUL => {
78                Ok(PropertyValue::Boolean(self.all_writes_successful))
79            }
80            p if p == PropertyIdentifier::ACTION => {
81                let items: Vec<PropertyValue> = self
82                    .action
83                    .iter()
84                    .map(|a| PropertyValue::OctetString(a.clone()))
85                    .collect();
86                Ok(PropertyValue::List(items))
87            }
88            _ => Err(common::unknown_property_error()),
89        }
90    }
91
92    fn write_property(
93        &mut self,
94        property: PropertyIdentifier,
95        _array_index: Option<u32>,
96        value: PropertyValue,
97        _priority: Option<u8>,
98    ) -> Result<(), Error> {
99        if let Some(result) =
100            common::write_out_of_service(&mut self.out_of_service, property, &value)
101        {
102            return result;
103        }
104        if let Some(result) = common::write_description(&mut self.description, property, &value) {
105            return result;
106        }
107        match property {
108            p if p == PropertyIdentifier::PRESENT_VALUE => {
109                if let PropertyValue::Unsigned(v) = value {
110                    self.present_value = v;
111                    // In a real system this would trigger action execution
112                    Ok(())
113                } else {
114                    Err(common::invalid_data_type_error())
115                }
116            }
117            p if p == PropertyIdentifier::ACTION => {
118                // ACTION is read-only from the network
119                Err(common::write_access_denied_error())
120            }
121            _ => Err(common::write_access_denied_error()),
122        }
123    }
124
125    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
126        static PROPS: &[PropertyIdentifier] = &[
127            PropertyIdentifier::OBJECT_IDENTIFIER,
128            PropertyIdentifier::OBJECT_NAME,
129            PropertyIdentifier::DESCRIPTION,
130            PropertyIdentifier::OBJECT_TYPE,
131            PropertyIdentifier::PRESENT_VALUE,
132            PropertyIdentifier::IN_PROCESS,
133            PropertyIdentifier::ALL_WRITES_SUCCESSFUL,
134            PropertyIdentifier::ACTION,
135            PropertyIdentifier::STATUS_FLAGS,
136            PropertyIdentifier::OUT_OF_SERVICE,
137            PropertyIdentifier::RELIABILITY,
138        ];
139        Cow::Borrowed(PROPS)
140    }
141}
142
143// ---------------------------------------------------------------------------
144// Tests
145// ---------------------------------------------------------------------------
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn command_create_and_read_defaults() {
153        let cmd = CommandObject::new(1, "CMD-1").unwrap();
154        assert_eq!(cmd.object_name(), "CMD-1");
155        assert_eq!(
156            cmd.read_property(PropertyIdentifier::PRESENT_VALUE, None)
157                .unwrap(),
158            PropertyValue::Unsigned(0)
159        );
160        assert_eq!(
161            cmd.read_property(PropertyIdentifier::IN_PROCESS, None)
162                .unwrap(),
163            PropertyValue::Boolean(false)
164        );
165        assert_eq!(
166            cmd.read_property(PropertyIdentifier::ALL_WRITES_SUCCESSFUL, None)
167                .unwrap(),
168            PropertyValue::Boolean(true)
169        );
170    }
171
172    #[test]
173    fn command_object_type() {
174        let cmd = CommandObject::new(1, "CMD-1").unwrap();
175        assert_eq!(
176            cmd.read_property(PropertyIdentifier::OBJECT_TYPE, None)
177                .unwrap(),
178            PropertyValue::Enumerated(ObjectType::COMMAND.to_raw())
179        );
180    }
181
182    #[test]
183    fn command_write_present_value() {
184        let mut cmd = CommandObject::new(1, "CMD-1").unwrap();
185        cmd.write_property(
186            PropertyIdentifier::PRESENT_VALUE,
187            None,
188            PropertyValue::Unsigned(3),
189            None,
190        )
191        .unwrap();
192        assert_eq!(
193            cmd.read_property(PropertyIdentifier::PRESENT_VALUE, None)
194                .unwrap(),
195            PropertyValue::Unsigned(3)
196        );
197    }
198
199    #[test]
200    fn command_write_present_value_wrong_type() {
201        let mut cmd = CommandObject::new(1, "CMD-1").unwrap();
202        let result = cmd.write_property(
203            PropertyIdentifier::PRESENT_VALUE,
204            None,
205            PropertyValue::Real(1.0),
206            None,
207        );
208        assert!(result.is_err());
209    }
210
211    #[test]
212    fn command_action_read_only() {
213        let mut cmd = CommandObject::new(1, "CMD-1").unwrap();
214        let result = cmd.write_property(
215            PropertyIdentifier::ACTION,
216            None,
217            PropertyValue::OctetString(vec![1, 2, 3]),
218            None,
219        );
220        assert!(result.is_err());
221    }
222
223    #[test]
224    fn command_read_action_empty() {
225        let cmd = CommandObject::new(1, "CMD-1").unwrap();
226        assert_eq!(
227            cmd.read_property(PropertyIdentifier::ACTION, None).unwrap(),
228            PropertyValue::List(vec![])
229        );
230    }
231
232    #[test]
233    fn command_read_action_with_data() {
234        let mut cmd = CommandObject::new(1, "CMD-1").unwrap();
235        cmd.set_action(vec![vec![1, 2, 3], vec![4, 5]]);
236        assert_eq!(
237            cmd.read_property(PropertyIdentifier::ACTION, None).unwrap(),
238            PropertyValue::List(vec![
239                PropertyValue::OctetString(vec![1, 2, 3]),
240                PropertyValue::OctetString(vec![4, 5]),
241            ])
242        );
243    }
244
245    #[test]
246    fn command_property_list() {
247        let cmd = CommandObject::new(1, "CMD-1").unwrap();
248        let list = cmd.property_list();
249        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
250        assert!(list.contains(&PropertyIdentifier::IN_PROCESS));
251        assert!(list.contains(&PropertyIdentifier::ALL_WRITES_SUCCESSFUL));
252        assert!(list.contains(&PropertyIdentifier::ACTION));
253    }
254}