Skip to main content

bacnet_objects/
load_control.rs

1//! Load Control object (type 28) per ASHRAE 135-2020 Clause 12.
2//!
3//! The Load Control object provides a standard interface for demand-response
4//! load shedding. It tracks requested, expected, and actual shed levels.
5
6use bacnet_types::constructed::BACnetShedLevel;
7use bacnet_types::enums::{ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
10use std::borrow::Cow;
11
12use crate::common::{self, read_common_properties};
13use crate::traits::BACnetObject;
14
15/// BACnet Load Control object — demand-response load shedding.
16pub struct LoadControlObject {
17    oid: ObjectIdentifier,
18    name: String,
19    description: String,
20    /// Present value: enumerated shed state (0=shed-inactive, 1=shed-request-pending,
21    /// 2=shed-compliant, 3=shed-non-compliant).
22    present_value: u32,
23    requested_shed_level: BACnetShedLevel,
24    expected_shed_level: BACnetShedLevel,
25    actual_shed_level: BACnetShedLevel,
26    shed_duration: u64,
27    start_time: (Date, Time),
28    status_flags: StatusFlags,
29    out_of_service: bool,
30    reliability: u32,
31}
32
33impl LoadControlObject {
34    /// Create a new Load Control object with default values.
35    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
36        let oid = ObjectIdentifier::new(ObjectType::LOAD_CONTROL, instance)?;
37        Ok(Self {
38            oid,
39            name: name.into(),
40            description: String::new(),
41            present_value: 0,
42            requested_shed_level: BACnetShedLevel::Percent(0),
43            expected_shed_level: BACnetShedLevel::Percent(0),
44            actual_shed_level: BACnetShedLevel::Percent(0),
45            shed_duration: 0,
46            start_time: (
47                Date {
48                    year: 0xFF,
49                    month: 0xFF,
50                    day: 0xFF,
51                    day_of_week: 0xFF,
52                },
53                Time {
54                    hour: 0xFF,
55                    minute: 0xFF,
56                    second: 0xFF,
57                    hundredths: 0xFF,
58                },
59            ),
60            status_flags: StatusFlags::empty(),
61            out_of_service: false,
62            reliability: 0,
63        })
64    }
65
66    /// Set the requested shed level.
67    pub fn set_requested_shed_level(&mut self, level: BACnetShedLevel) {
68        self.requested_shed_level = level;
69    }
70
71    /// Set the actual shed level.
72    pub fn set_actual_shed_level(&mut self, level: BACnetShedLevel) {
73        self.actual_shed_level = level;
74    }
75
76    /// Encode a BACnetShedLevel to a PropertyValue.
77    fn shed_level_to_property(level: &BACnetShedLevel) -> PropertyValue {
78        match level {
79            BACnetShedLevel::Percent(v) => {
80                PropertyValue::List(vec![PropertyValue::Unsigned(*v as u64)])
81            }
82            BACnetShedLevel::Level(v) => {
83                PropertyValue::List(vec![PropertyValue::Unsigned(*v as u64)])
84            }
85            BACnetShedLevel::Amount(v) => PropertyValue::List(vec![PropertyValue::Real(*v)]),
86        }
87    }
88}
89
90impl BACnetObject for LoadControlObject {
91    fn object_identifier(&self) -> ObjectIdentifier {
92        self.oid
93    }
94
95    fn object_name(&self) -> &str {
96        &self.name
97    }
98
99    fn read_property(
100        &self,
101        property: PropertyIdentifier,
102        array_index: Option<u32>,
103    ) -> Result<PropertyValue, Error> {
104        if let Some(result) = read_common_properties!(self, property, array_index) {
105            return result;
106        }
107        match property {
108            p if p == PropertyIdentifier::OBJECT_TYPE => {
109                Ok(PropertyValue::Enumerated(ObjectType::LOAD_CONTROL.to_raw()))
110            }
111            p if p == PropertyIdentifier::PRESENT_VALUE => {
112                Ok(PropertyValue::Enumerated(self.present_value))
113            }
114            p if p == PropertyIdentifier::REQUESTED_SHED_LEVEL => {
115                Ok(Self::shed_level_to_property(&self.requested_shed_level))
116            }
117            p if p == PropertyIdentifier::EXPECTED_SHED_LEVEL => {
118                Ok(Self::shed_level_to_property(&self.expected_shed_level))
119            }
120            p if p == PropertyIdentifier::ACTUAL_SHED_LEVEL => {
121                Ok(Self::shed_level_to_property(&self.actual_shed_level))
122            }
123            p if p == PropertyIdentifier::SHED_DURATION => {
124                Ok(PropertyValue::Unsigned(self.shed_duration))
125            }
126            p if p == PropertyIdentifier::START_TIME => Ok(PropertyValue::List(vec![
127                PropertyValue::Date(self.start_time.0),
128                PropertyValue::Time(self.start_time.1),
129            ])),
130            _ => Err(common::unknown_property_error()),
131        }
132    }
133
134    fn write_property(
135        &mut self,
136        property: PropertyIdentifier,
137        _array_index: Option<u32>,
138        value: PropertyValue,
139        _priority: Option<u8>,
140    ) -> Result<(), Error> {
141        if let Some(result) =
142            common::write_out_of_service(&mut self.out_of_service, property, &value)
143        {
144            return result;
145        }
146        if let Some(result) = common::write_description(&mut self.description, property, &value) {
147            return result;
148        }
149        match property {
150            p if p == PropertyIdentifier::SHED_DURATION => {
151                if let PropertyValue::Unsigned(v) = value {
152                    self.shed_duration = v;
153                    Ok(())
154                } else {
155                    Err(common::invalid_data_type_error())
156                }
157            }
158            p if p == PropertyIdentifier::REQUESTED_SHED_LEVEL => {
159                // Accept List with a single Unsigned (percent/level) or Real (amount)
160                if let PropertyValue::List(ref items) = value {
161                    if items.len() == 1 {
162                        match &items[0] {
163                            PropertyValue::Unsigned(v) => {
164                                self.requested_shed_level =
165                                    BACnetShedLevel::Percent(common::u64_to_u32(*v)?);
166                                Ok(())
167                            }
168                            PropertyValue::Real(v) => {
169                                common::reject_non_finite(*v)?;
170                                self.requested_shed_level = BACnetShedLevel::Amount(*v);
171                                Ok(())
172                            }
173                            _ => Err(common::invalid_data_type_error()),
174                        }
175                    } else {
176                        Err(common::invalid_data_type_error())
177                    }
178                } else {
179                    Err(common::invalid_data_type_error())
180                }
181            }
182            _ => Err(common::write_access_denied_error()),
183        }
184    }
185
186    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
187        static PROPS: &[PropertyIdentifier] = &[
188            PropertyIdentifier::OBJECT_IDENTIFIER,
189            PropertyIdentifier::OBJECT_NAME,
190            PropertyIdentifier::DESCRIPTION,
191            PropertyIdentifier::OBJECT_TYPE,
192            PropertyIdentifier::PRESENT_VALUE,
193            PropertyIdentifier::REQUESTED_SHED_LEVEL,
194            PropertyIdentifier::EXPECTED_SHED_LEVEL,
195            PropertyIdentifier::ACTUAL_SHED_LEVEL,
196            PropertyIdentifier::SHED_DURATION,
197            PropertyIdentifier::START_TIME,
198            PropertyIdentifier::STATUS_FLAGS,
199            PropertyIdentifier::OUT_OF_SERVICE,
200            PropertyIdentifier::RELIABILITY,
201        ];
202        Cow::Borrowed(PROPS)
203    }
204}
205
206// ---------------------------------------------------------------------------
207// Tests
208// ---------------------------------------------------------------------------
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn load_control_create_and_read_defaults() {
216        let lc = LoadControlObject::new(1, "LC-1").unwrap();
217        assert_eq!(lc.object_name(), "LC-1");
218        assert_eq!(
219            lc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
220                .unwrap(),
221            PropertyValue::Enumerated(0)
222        );
223    }
224
225    #[test]
226    fn load_control_object_type() {
227        let lc = LoadControlObject::new(1, "LC-1").unwrap();
228        assert_eq!(
229            lc.read_property(PropertyIdentifier::OBJECT_TYPE, None)
230                .unwrap(),
231            PropertyValue::Enumerated(ObjectType::LOAD_CONTROL.to_raw())
232        );
233    }
234
235    #[test]
236    fn load_control_read_shed_levels() {
237        let lc = LoadControlObject::new(1, "LC-1").unwrap();
238        // Default is Percent(0)
239        assert_eq!(
240            lc.read_property(PropertyIdentifier::REQUESTED_SHED_LEVEL, None)
241                .unwrap(),
242            PropertyValue::List(vec![PropertyValue::Unsigned(0)])
243        );
244        assert_eq!(
245            lc.read_property(PropertyIdentifier::EXPECTED_SHED_LEVEL, None)
246                .unwrap(),
247            PropertyValue::List(vec![PropertyValue::Unsigned(0)])
248        );
249        assert_eq!(
250            lc.read_property(PropertyIdentifier::ACTUAL_SHED_LEVEL, None)
251                .unwrap(),
252            PropertyValue::List(vec![PropertyValue::Unsigned(0)])
253        );
254    }
255
256    #[test]
257    fn load_control_set_requested_shed_level_amount() {
258        let mut lc = LoadControlObject::new(1, "LC-1").unwrap();
259        lc.set_requested_shed_level(BACnetShedLevel::Amount(42.5));
260        assert_eq!(
261            lc.read_property(PropertyIdentifier::REQUESTED_SHED_LEVEL, None)
262                .unwrap(),
263            PropertyValue::List(vec![PropertyValue::Real(42.5)])
264        );
265    }
266
267    #[test]
268    fn load_control_write_shed_duration() {
269        let mut lc = LoadControlObject::new(1, "LC-1").unwrap();
270        lc.write_property(
271            PropertyIdentifier::SHED_DURATION,
272            None,
273            PropertyValue::Unsigned(3600),
274            None,
275        )
276        .unwrap();
277        assert_eq!(
278            lc.read_property(PropertyIdentifier::SHED_DURATION, None)
279                .unwrap(),
280            PropertyValue::Unsigned(3600)
281        );
282    }
283
284    #[test]
285    fn load_control_write_requested_shed_level() {
286        let mut lc = LoadControlObject::new(1, "LC-1").unwrap();
287        lc.write_property(
288            PropertyIdentifier::REQUESTED_SHED_LEVEL,
289            None,
290            PropertyValue::List(vec![PropertyValue::Unsigned(50)]),
291            None,
292        )
293        .unwrap();
294        assert_eq!(
295            lc.read_property(PropertyIdentifier::REQUESTED_SHED_LEVEL, None)
296                .unwrap(),
297            PropertyValue::List(vec![PropertyValue::Unsigned(50)])
298        );
299    }
300
301    #[test]
302    fn load_control_write_requested_shed_level_amount() {
303        let mut lc = LoadControlObject::new(1, "LC-1").unwrap();
304        lc.write_property(
305            PropertyIdentifier::REQUESTED_SHED_LEVEL,
306            None,
307            PropertyValue::List(vec![PropertyValue::Real(25.5)]),
308            None,
309        )
310        .unwrap();
311        assert_eq!(
312            lc.read_property(PropertyIdentifier::REQUESTED_SHED_LEVEL, None)
313                .unwrap(),
314            PropertyValue::List(vec![PropertyValue::Real(25.5)])
315        );
316    }
317
318    #[test]
319    fn load_control_write_requested_shed_level_wrong_type() {
320        let mut lc = LoadControlObject::new(1, "LC-1").unwrap();
321        let result = lc.write_property(
322            PropertyIdentifier::REQUESTED_SHED_LEVEL,
323            None,
324            PropertyValue::Unsigned(50),
325            None,
326        );
327        assert!(result.is_err());
328    }
329
330    #[test]
331    fn load_control_read_start_time() {
332        let lc = LoadControlObject::new(1, "LC-1").unwrap();
333        let val = lc
334            .read_property(PropertyIdentifier::START_TIME, None)
335            .unwrap();
336        let unspec_date = Date {
337            year: 0xFF,
338            month: 0xFF,
339            day: 0xFF,
340            day_of_week: 0xFF,
341        };
342        let unspec_time = Time {
343            hour: 0xFF,
344            minute: 0xFF,
345            second: 0xFF,
346            hundredths: 0xFF,
347        };
348        assert_eq!(
349            val,
350            PropertyValue::List(vec![
351                PropertyValue::Date(unspec_date),
352                PropertyValue::Time(unspec_time),
353            ])
354        );
355    }
356
357    #[test]
358    fn load_control_property_list() {
359        let lc = LoadControlObject::new(1, "LC-1").unwrap();
360        let list = lc.property_list();
361        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
362        assert!(list.contains(&PropertyIdentifier::REQUESTED_SHED_LEVEL));
363        assert!(list.contains(&PropertyIdentifier::EXPECTED_SHED_LEVEL));
364        assert!(list.contains(&PropertyIdentifier::ACTUAL_SHED_LEVEL));
365        assert!(list.contains(&PropertyIdentifier::SHED_DURATION));
366        assert!(list.contains(&PropertyIdentifier::START_TIME));
367    }
368}