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