Skip to main content

bacnet_objects/
load_control.rs

1//! Load Control object (type 28) per ASHRAE 135-2020 Clause 12.28.
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_encoding::constructed::{decode_shed_level, encode_shed_level};
7use bacnet_types::constructed::BACnetShedLevel;
8use bacnet_types::enums::{EventState, ObjectType, PropertyIdentifier, Reliability};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
11use bytes::BytesMut;
12use std::borrow::Cow;
13
14use crate::common::{self, read_common_properties};
15use crate::traits::BACnetObject;
16
17mod metadata;
18#[cfg(test)]
19mod tests;
20
21/// BACnet Load Control object — demand-response load shedding.
22///
23/// Requested_Shed_Level, Expected_Shed_Level and Actual_Shed_Level are
24/// [`BACnetShedLevel`] values and go out in that CHOICE's context-tagged form
25/// (Clause 21). The object runs no shed state machine: Start_Time has no write
26/// route (#1092), so Present_Value never leaves SHED_INACTIVE, and while it is
27/// there Clauses 12.28.16 and 12.28.17 pin the expected and actual levels to
28/// the Table 12-33 default of the choice the requested level uses.
29pub struct LoadControlObject {
30    oid: ObjectIdentifier,
31    name: String,
32    description: String,
33    /// Present value: enumerated shed state (0=shed-inactive, 1=shed-request-pending,
34    /// 2=shed-compliant, 3=shed-non-compliant).
35    present_value: u32,
36    requested_shed_level: BACnetShedLevel,
37    expected_shed_level: BACnetShedLevel,
38    actual_shed_level: BACnetShedLevel,
39    shed_duration: u64,
40    start_time: (Date, Time),
41    status_flags: StatusFlags,
42    /// Event_State.
43    event_state: EventState,
44    reliability: Reliability,
45}
46
47impl LoadControlObject {
48    /// Create a new Load Control object with default values.
49    ///
50    /// The three shed levels start at level 0: LEVEL is the one choice every
51    /// Load Control must take (Clause 12.28.10), and 0 is its no-shed default.
52    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
53        let oid = ObjectIdentifier::new(ObjectType::LOAD_CONTROL, instance)?;
54        Ok(Self {
55            oid,
56            name: name.into(),
57            description: String::new(),
58            present_value: 0,
59            requested_shed_level: BACnetShedLevel::Level(0),
60            expected_shed_level: BACnetShedLevel::Level(0),
61            actual_shed_level: BACnetShedLevel::Level(0),
62            shed_duration: 0,
63            start_time: (
64                Date {
65                    year: 0xFF,
66                    month: 0xFF,
67                    day: 0xFF,
68                    day_of_week: 0xFF,
69                },
70                Time {
71                    hour: 0xFF,
72                    minute: 0xFF,
73                    second: 0xFF,
74                    hundredths: 0xFF,
75                },
76            ),
77            status_flags: StatusFlags::empty(),
78            event_state: EventState::NORMAL,
79            reliability: Reliability::NO_FAULT_DETECTED,
80        })
81    }
82
83    /// Set Requested_Shed_Level, with the checks and effects of a
84    /// WriteProperty of it.
85    ///
86    /// A shed request asks for less load, so a percent above 100, or an amount
87    /// that is negative or not finite, would ask for more than the baseline
88    /// (Table 12-33) and fails with PROPERTY / VALUE_OUT_OF_RANGE, changing
89    /// nothing. Any level is accepted. As the object stays SHED_INACTIVE,
90    /// Expected_Shed_Level and Actual_Shed_Level then take the default of the
91    /// new level's choice: 100 for a percent, 0 for a level, 0.0 for an amount.
92    pub fn set_requested_shed_level(&mut self, level: BACnetShedLevel) -> Result<(), Error> {
93        match level {
94            BACnetShedLevel::Percent(percent) if percent > 100 => {
95                return Err(common::value_out_of_range_error());
96            }
97            BACnetShedLevel::Amount(amount) => {
98                common::reject_non_finite(amount)?;
99                if amount < 0.0 {
100                    return Err(common::value_out_of_range_error());
101                }
102            }
103            _ => {}
104        }
105        let default = choice_default(&level);
106        self.expected_shed_level = default.clone();
107        self.actual_shed_level = default;
108        self.requested_shed_level = level;
109        Ok(())
110    }
111
112    /// Set Actual_Shed_Level, the shed the application reports achieving.
113    ///
114    /// It is in Requested_Shed_Level's units (Clause 12.28.17), so a level of
115    /// another choice, or an amount that isn't finite, fails with PROPERTY /
116    /// VALUE_OUT_OF_RANGE and changes nothing.
117    pub fn set_actual_shed_level(&mut self, level: BACnetShedLevel) -> Result<(), Error> {
118        if std::mem::discriminant(&level) != std::mem::discriminant(&self.requested_shed_level) {
119            return Err(common::value_out_of_range_error());
120        }
121        if let BACnetShedLevel::Amount(amount) = level {
122            common::reject_non_finite(amount)?;
123        }
124        self.actual_shed_level = level;
125        Ok(())
126    }
127}
128
129/// The Table 12-33 default for the choice `level` uses, the value that also
130/// cancels a shed request when written.
131fn choice_default(level: &BACnetShedLevel) -> BACnetShedLevel {
132    match level {
133        BACnetShedLevel::Percent(_) => BACnetShedLevel::Percent(100),
134        BACnetShedLevel::Level(_) => BACnetShedLevel::Level(0),
135        BACnetShedLevel::Amount(_) => BACnetShedLevel::Amount(0.0),
136    }
137}
138
139/// A shed level in its Clause 21 CHOICE form.
140fn shed_level_value(level: &BACnetShedLevel) -> PropertyValue {
141    let mut buf = BytesMut::new();
142    encode_shed_level(&mut buf, level);
143    PropertyValue::ApplicationData(buf.to_vec())
144}
145
146/// Decode a Requested_Shed_Level write: exactly one `BACnetShedLevel` CHOICE,
147/// which the WriteProperty decoder passes on as `ApplicationData`. Any other
148/// value, the bare Unsigned or REAL and the one-element list taken before
149/// #1133 included, fails with PROPERTY / INVALID_DATA_TYPE.
150fn decode_shed_level_write(value: PropertyValue) -> Result<BACnetShedLevel, Error> {
151    let PropertyValue::ApplicationData(bytes) = value else {
152        return Err(common::invalid_data_type_error());
153    };
154    match decode_shed_level(&bytes, 0) {
155        Ok((level, end)) if end == bytes.len() => Ok(level),
156        _ => Err(common::invalid_data_type_error()),
157    }
158}
159
160impl BACnetObject for LoadControlObject {
161    fn object_identifier(&self) -> ObjectIdentifier {
162        self.oid
163    }
164
165    fn object_name(&self) -> &str {
166        &self.name
167    }
168
169    fn read_property(
170        &self,
171        property: PropertyIdentifier,
172        array_index: Option<u32>,
173    ) -> Result<PropertyValue, Error> {
174        // Table 12-32 has no Out_Of_Service (#1064), and Clause 12.28 holds the
175        // OUT_OF_SERVICE flag FALSE.
176        if let Some(result) =
177            read_common_properties!(self, property, array_index, no_out_of_service)
178        {
179            return result;
180        }
181        match property {
182            p if p == PropertyIdentifier::OBJECT_TYPE => {
183                Ok(PropertyValue::Enumerated(ObjectType::LOAD_CONTROL.to_raw()))
184            }
185            p if p == PropertyIdentifier::PRESENT_VALUE => {
186                Ok(PropertyValue::Enumerated(self.present_value))
187            }
188            p if p == PropertyIdentifier::REQUESTED_SHED_LEVEL => {
189                Ok(shed_level_value(&self.requested_shed_level))
190            }
191            p if p == PropertyIdentifier::EXPECTED_SHED_LEVEL => {
192                Ok(shed_level_value(&self.expected_shed_level))
193            }
194            p if p == PropertyIdentifier::ACTUAL_SHED_LEVEL => {
195                Ok(shed_level_value(&self.actual_shed_level))
196            }
197            p if p == PropertyIdentifier::SHED_DURATION => {
198                Ok(PropertyValue::Unsigned(self.shed_duration))
199            }
200            p if p == PropertyIdentifier::START_TIME => Ok(PropertyValue::List(vec![
201                PropertyValue::Date(self.start_time.0),
202                PropertyValue::Time(self.start_time.1),
203            ])),
204            p if p == PropertyIdentifier::EVENT_STATE => {
205                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
206            }
207            _ => Err(common::unknown_property_error()),
208        }
209    }
210
211    fn write_property(
212        &mut self,
213        property: PropertyIdentifier,
214        _array_index: Option<u32>,
215        value: PropertyValue,
216        _priority: Option<u8>,
217    ) -> Result<(), Error> {
218        if let Some(result) = common::write_description(&mut self.description, property, &value) {
219            return result;
220        }
221        match property {
222            p if p == PropertyIdentifier::SHED_DURATION => {
223                if let PropertyValue::Unsigned(v) = value {
224                    self.shed_duration = v;
225                    Ok(())
226                } else {
227                    Err(common::invalid_data_type_error())
228                }
229            }
230            p if p == PropertyIdentifier::REQUESTED_SHED_LEVEL => {
231                self.set_requested_shed_level(decode_shed_level_write(value)?)
232            }
233            _ => Err(crate::common::unhandled_write_error(
234                self.property_metadata().as_ref(),
235                property,
236                _array_index,
237            )),
238        }
239    }
240
241    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
242        metadata::for_object(self)
243    }
244
245    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
246        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
247    }
248
249    /// Table 13-1 lists Load Control, so it takes SubscribeCOV; the shed
250    /// rows it reports come from the trait default.
251    fn supports_cov(&self) -> bool {
252        true
253    }
254}