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