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