Skip to main content

bacnet_objects/
elevator.rs

1//! Elevator Group, Escalator, and Lift objects per ASHRAE 135-2020.
2//!
3//! - ElevatorGroupObject (type 57) — Clause 12.58
4//! - EscalatorObject (type 58) — Clause 12.60
5//! - LiftObject (type 59) — Clause 12.59
6
7use bacnet_types::constructed::BACnetLandingCallStatus;
8use bacnet_types::enums::{LiftCarDirection, LiftGroupMode, ObjectType, PropertyIdentifier};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
11use std::{borrow::Cow, collections::HashSet, hash::Hash};
12
13use crate::common;
14use crate::traits::BACnetObject;
15
16mod car_state;
17mod door_values;
18mod doors;
19mod energy_meter;
20mod escalator;
21mod landing_calls;
22mod lift;
23mod membership;
24mod metadata;
25
26pub use escalator::EscalatorObject;
27pub use lift::LiftObject;
28
29/// Whether `raw` is in the domain of one of this family's extensible Clause 21
30/// enumerations: one of its named values (`named` is its `ALL_NAMED` table,
31/// `value` is `raw` as that type), or the proprietary range 1024..=65535 that
32/// Clause 23.1 (Table 23-1) opens for each of them. Every other value is
33/// reserved or too large, and a write of it is refused with
34/// VALUE_OUT_OF_RANGE.
35fn named_or_proprietary<T: Copy + PartialEq>(named: &[(&str, T)], value: T, raw: u32) -> bool {
36    named.iter().any(|&(_, named)| named == value) || (1024..=65_535).contains(&raw)
37}
38
39/// Whether `direction` is in BACnetLiftCarDirection, the datatype of
40/// Car_Moving_Direction, Car_Assigned_Direction and the direction of each
41/// landing call.
42fn direction_in_range(direction: &LiftCarDirection) -> bool {
43    named_or_proprietary(LiftCarDirection::ALL_NAMED, *direction, direction.to_raw())
44}
45
46/// `value`, or VALUE_OUT_OF_RANGE when `in_range` refuses it.
47fn checked<T>(value: T, in_range: fn(&T) -> bool) -> Result<T, Error> {
48    if in_range(&value) {
49        Ok(value)
50    } else {
51        Err(common::value_out_of_range_error())
52    }
53}
54
55/// Decode a Fault_Signals write, a BACnetLIST of one of this family's fault
56/// enumerations (`named` is its `ALL_NAMED` table, `from_raw` its
57/// constructor): one Enumerated or a list of them. A value that isn't
58/// Enumerated is INVALID_DATA_TYPE; a fault outside the enumeration's domain
59/// or a repeated fault (the list holds a set) is VALUE_OUT_OF_RANGE. The
60/// caller stores nothing unless every element passes.
61fn decode_fault_signals<T: Copy + Eq + Hash>(
62    value: PropertyValue,
63    named: &[(&str, T)],
64    from_raw: fn(u32) -> T,
65) -> Result<Vec<T>, Error> {
66    let values = match value {
67        PropertyValue::Enumerated(v) => vec![PropertyValue::Enumerated(v)],
68        PropertyValue::List(values) => values,
69        _ => return Err(common::invalid_data_type_error()),
70    };
71    let mut faults = Vec::with_capacity(values.len());
72    let mut seen = HashSet::with_capacity(values.len());
73    // A refusal names its element (#1048).
74    for (index, value) in values.into_iter().enumerate() {
75        let PropertyValue::Enumerated(raw) = value else {
76            return Err(common::at_list_element(
77                common::invalid_data_type_error(),
78                index,
79            ));
80        };
81        let fault = from_raw(raw);
82        if !named_or_proprietary(named, fault, raw) || !seen.insert(fault) {
83            return Err(common::at_list_element(
84                common::value_out_of_range_error(),
85                index,
86            ));
87        }
88        faults.push(fault);
89    }
90    Ok(faults)
91}
92
93// ===========================================================================
94// ElevatorGroupObject (type 57)
95// ===========================================================================
96
97/// BACnet Elevator Group object — manages a group of lifts.
98///
99/// The object serves only properties its table (Clause 12.58, Table 12-76)
100/// defines. That table has no Status_Flags, Out_Of_Service or Reliability, so
101/// reads and writes of those return UNKNOWN_PROPERTY.
102pub struct ElevatorGroupObject {
103    oid: ObjectIdentifier,
104    name: String,
105    description: String,
106    /// The Positive Integer Value object holding the machine room's number.
107    machine_room_id: ObjectIdentifier,
108    /// Group identifier (Unsigned8).
109    group_id: u8,
110    /// List of lift ObjectIdentifiers in this group.
111    group_members: Vec<ObjectIdentifier>,
112    /// Group mode.
113    group_mode: LiftGroupMode,
114    /// Active landing calls, served as the Landing_Calls BACnetLIST.
115    landing_calls: Vec<BACnetLandingCallStatus>,
116    /// The last call written to Landing_Call_Control.
117    landing_call_control: BACnetLandingCallStatus,
118}
119
120impl ElevatorGroupObject {
121    /// Create a new Elevator Group object with default values.
122    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
123        let oid = ObjectIdentifier::new(ObjectType::ELEVATOR_GROUP, instance)?;
124        Ok(Self {
125            oid,
126            name: name.into(),
127            description: String::new(),
128            machine_room_id: ObjectIdentifier::new(
129                ObjectType::POSITIVE_INTEGER_VALUE,
130                ObjectIdentifier::MAX_INSTANCE,
131            )?,
132            group_id: 0,
133            group_members: Vec::new(),
134            group_mode: LiftGroupMode::UNKNOWN,
135            landing_calls: Vec::new(),
136            landing_call_control: landing_calls::initial_landing_call_control(),
137        })
138    }
139
140    /// The Positive Integer Value object served as Machine_Room_ID. Until the
141    /// application sets one it names instance 4194303, which Clause 12.58
142    /// uses when the machine room has no identification number.
143    pub fn machine_room_id(&self) -> ObjectIdentifier {
144        self.machine_room_id
145    }
146
147    /// Choose which Positive Integer Value object supplies this group's
148    /// machine-room number; clients read the number from that object's
149    /// Present_Value (Clause 12.58).
150    ///
151    /// Machine_Room_ID is read-only over the network, so this is the only way
152    /// to change it. A reference to any other object type is refused with
153    /// VALUE_OUT_OF_RANGE and the property is left unchanged.
154    pub fn set_machine_room_id(&mut self, oid: ObjectIdentifier) -> Result<(), Error> {
155        if oid.object_type() != ObjectType::POSITIVE_INTEGER_VALUE {
156            return Err(common::value_out_of_range_error());
157        }
158        self.machine_room_id = oid;
159        Ok(())
160    }
161
162    /// Add a lift member to this elevator group.
163    pub fn add_member(&mut self, oid: ObjectIdentifier) {
164        self.group_members.push(oid);
165    }
166
167    /// The last landing call written to Landing_Call_Control, or a placeholder
168    /// of floor 0 with direction UNKNOWN before any write.
169    pub fn landing_call_control(&self) -> &BACnetLandingCallStatus {
170        &self.landing_call_control
171    }
172
173    /// The active landing calls served as Landing_Calls.
174    pub fn landing_calls(&self) -> &[BACnetLandingCallStatus] {
175        &self.landing_calls
176    }
177
178    /// Replace the active landing calls served as Landing_Calls.
179    ///
180    /// The application owns this list: a Landing_Call_Control write doesn't
181    /// add to it. A call with a direction outside BACnetLiftCarDirection is
182    /// refused with VALUE_OUT_OF_RANGE and the list is left unchanged.
183    pub fn set_landing_calls(&mut self, calls: Vec<BACnetLandingCallStatus>) -> Result<(), Error> {
184        calls.iter().try_for_each(landing_calls::validate)?;
185        self.landing_calls = calls;
186        Ok(())
187    }
188}
189
190impl BACnetObject for ElevatorGroupObject {
191    fn object_identifier(&self) -> ObjectIdentifier {
192        self.oid
193    }
194
195    fn object_name(&self) -> &str {
196        &self.name
197    }
198
199    fn read_property(
200        &self,
201        property: PropertyIdentifier,
202        array_index: Option<u32>,
203    ) -> Result<PropertyValue, Error> {
204        // No read_common_properties!: it would serve Status_Flags,
205        // Out_Of_Service and Reliability, which Table 12-76 doesn't define.
206        match property {
207            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
208                Ok(PropertyValue::ObjectIdentifier(self.oid))
209            }
210            p if p == PropertyIdentifier::OBJECT_NAME => {
211                Ok(PropertyValue::CharacterString(self.name.clone()))
212            }
213            p if p == PropertyIdentifier::DESCRIPTION => {
214                Ok(PropertyValue::CharacterString(self.description.clone()))
215            }
216            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
217                ObjectType::ELEVATOR_GROUP.to_raw(),
218            )),
219            p if p == PropertyIdentifier::PROPERTY_LIST => {
220                common::read_property_list_property(&self.property_list(), array_index)
221            }
222            p if p == PropertyIdentifier::MACHINE_ROOM_ID => {
223                Ok(PropertyValue::ObjectIdentifier(self.machine_room_id))
224            }
225            p if p == PropertyIdentifier::GROUP_ID => {
226                Ok(PropertyValue::Unsigned(u64::from(self.group_id)))
227            }
228            // A BACnetARRAY (Table 12-76): the whole array, its size at
229            // index 0, or one member.
230            p if p == PropertyIdentifier::GROUP_MEMBERS => common::read_array(
231                self.group_members
232                    .iter()
233                    .copied()
234                    .map(PropertyValue::ObjectIdentifier)
235                    .collect(),
236                array_index,
237            ),
238            p if p == PropertyIdentifier::GROUP_MODE => {
239                Ok(PropertyValue::Enumerated(self.group_mode.to_raw()))
240            }
241            p if p == PropertyIdentifier::LANDING_CALLS => Ok(PropertyValue::List(
242                self.landing_calls
243                    .iter()
244                    .map(landing_calls::encode)
245                    .collect::<Result<Vec<_>, Error>>()?,
246            )),
247            p if p == PropertyIdentifier::LANDING_CALL_CONTROL => {
248                landing_calls::encode(&self.landing_call_control)
249            }
250            _ => Err(common::unknown_property_error()),
251        }
252    }
253
254    fn write_property(
255        &mut self,
256        property: PropertyIdentifier,
257        _array_index: Option<u32>,
258        value: PropertyValue,
259        _priority: Option<u8>,
260    ) -> Result<(), Error> {
261        if let Some(result) = common::write_description(&mut self.description, property, &value) {
262            return result;
263        }
264        match property {
265            p if p == PropertyIdentifier::GROUP_ID => {
266                if let PropertyValue::Unsigned(v) = value {
267                    // Group_ID is an Unsigned8 (Table 12-76).
268                    self.group_id =
269                        u8::try_from(v).map_err(|_| common::value_out_of_range_error())?;
270                    Ok(())
271                } else {
272                    Err(common::invalid_data_type_error())
273                }
274            }
275            p if p == PropertyIdentifier::GROUP_MODE => {
276                if let PropertyValue::Enumerated(v) = value {
277                    self.group_mode = LiftGroupMode::from_raw(v);
278                    Ok(())
279                } else {
280                    Err(common::invalid_data_type_error())
281                }
282            }
283            p if p == PropertyIdentifier::LANDING_CALL_CONTROL => {
284                self.landing_call_control = landing_calls::decode_write(value)?;
285                Ok(())
286            }
287            _ => Err(crate::common::unhandled_write_error(
288                self.property_metadata().as_ref(),
289                property,
290                _array_index,
291            )),
292        }
293    }
294
295    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
296        metadata::for_elevator_group_object(self)
297    }
298
299    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
300        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
301    }
302}
303
304// ---------------------------------------------------------------------------
305// Tests
306// ---------------------------------------------------------------------------
307
308#[cfg(test)]
309mod tests;