Skip to main content

bacnet_objects/
elevator.rs

1//! Elevator, Escalator, and Lift objects per ASHRAE 135-2020 Clause 12.
2//!
3//! - ElevatorGroupObject (type 57): manages a group of lifts
4//! - EscalatorObject (type 58): represents an escalator
5//! - LiftObject (type 59): represents a single lift/elevator car
6
7use bacnet_types::enums::{
8    EscalatorFault, EscalatorMode, EscalatorOperationDirection, ObjectType, PropertyIdentifier,
9};
10use bacnet_types::error::Error;
11use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
12use std::{borrow::Cow, collections::HashSet};
13
14use crate::common::{self, read_common_properties};
15use crate::traits::BACnetObject;
16
17// ===========================================================================
18// ElevatorGroupObject (type 57)
19// ===========================================================================
20
21/// BACnet Elevator Group object — manages a group of lifts.
22pub struct ElevatorGroupObject {
23    oid: ObjectIdentifier,
24    name: String,
25    description: String,
26    /// Group identifier (Unsigned).
27    group_id: u64,
28    /// List of lift ObjectIdentifiers in this group.
29    group_members: Vec<ObjectIdentifier>,
30    /// Group mode (LiftGroupMode enumerated value).
31    group_mode: u32,
32    /// Number of landing calls (stored as count).
33    landing_calls: u64,
34    /// Landing call control (Enumerated).
35    landing_call_control: u32,
36    status_flags: StatusFlags,
37    out_of_service: bool,
38    reliability: u32,
39}
40
41impl ElevatorGroupObject {
42    /// Create a new Elevator Group object with default values.
43    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
44        let oid = ObjectIdentifier::new(ObjectType::ELEVATOR_GROUP, instance)?;
45        Ok(Self {
46            oid,
47            name: name.into(),
48            description: String::new(),
49            group_id: 0,
50            group_members: Vec::new(),
51            group_mode: 0, // Unknown
52            landing_calls: 0,
53            landing_call_control: 0,
54            status_flags: StatusFlags::empty(),
55            out_of_service: false,
56            reliability: 0,
57        })
58    }
59
60    /// Add a lift member to this elevator group.
61    pub fn add_member(&mut self, oid: ObjectIdentifier) {
62        self.group_members.push(oid);
63    }
64}
65
66impl BACnetObject for ElevatorGroupObject {
67    fn object_identifier(&self) -> ObjectIdentifier {
68        self.oid
69    }
70
71    fn object_name(&self) -> &str {
72        &self.name
73    }
74
75    fn read_property(
76        &self,
77        property: PropertyIdentifier,
78        array_index: Option<u32>,
79    ) -> Result<PropertyValue, Error> {
80        if let Some(result) = read_common_properties!(self, property, array_index) {
81            return result;
82        }
83        match property {
84            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
85                ObjectType::ELEVATOR_GROUP.to_raw(),
86            )),
87            p if p == PropertyIdentifier::GROUP_ID => Ok(PropertyValue::Unsigned(self.group_id)),
88            p if p == PropertyIdentifier::GROUP_MEMBERS => {
89                let items: Vec<PropertyValue> = self
90                    .group_members
91                    .iter()
92                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
93                    .collect();
94                Ok(PropertyValue::List(items))
95            }
96            p if p == PropertyIdentifier::GROUP_MODE => {
97                Ok(PropertyValue::Enumerated(self.group_mode))
98            }
99            p if p == PropertyIdentifier::LANDING_CALLS => {
100                Ok(PropertyValue::Unsigned(self.landing_calls))
101            }
102            p if p == PropertyIdentifier::LANDING_CALL_CONTROL => {
103                Ok(PropertyValue::Enumerated(self.landing_call_control))
104            }
105            _ => Err(common::unknown_property_error()),
106        }
107    }
108
109    fn write_property(
110        &mut self,
111        property: PropertyIdentifier,
112        _array_index: Option<u32>,
113        value: PropertyValue,
114        _priority: Option<u8>,
115    ) -> Result<(), Error> {
116        if let Some(result) =
117            common::write_out_of_service(&mut self.out_of_service, property, &value)
118        {
119            return result;
120        }
121        if let Some(result) = common::write_description(&mut self.description, property, &value) {
122            return result;
123        }
124        match property {
125            p if p == PropertyIdentifier::GROUP_ID => {
126                if let PropertyValue::Unsigned(v) = value {
127                    self.group_id = v;
128                    Ok(())
129                } else {
130                    Err(common::invalid_data_type_error())
131                }
132            }
133            p if p == PropertyIdentifier::GROUP_MODE => {
134                if let PropertyValue::Enumerated(v) = value {
135                    self.group_mode = v;
136                    Ok(())
137                } else {
138                    Err(common::invalid_data_type_error())
139                }
140            }
141            p if p == PropertyIdentifier::LANDING_CALL_CONTROL => {
142                if let PropertyValue::Enumerated(v) = value {
143                    self.landing_call_control = v;
144                    Ok(())
145                } else {
146                    Err(common::invalid_data_type_error())
147                }
148            }
149            _ => Err(common::write_access_denied_error()),
150        }
151    }
152
153    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
154        static PROPS: &[PropertyIdentifier] = &[
155            PropertyIdentifier::OBJECT_IDENTIFIER,
156            PropertyIdentifier::OBJECT_NAME,
157            PropertyIdentifier::DESCRIPTION,
158            PropertyIdentifier::OBJECT_TYPE,
159            PropertyIdentifier::GROUP_ID,
160            PropertyIdentifier::GROUP_MEMBERS,
161            PropertyIdentifier::GROUP_MODE,
162            PropertyIdentifier::LANDING_CALLS,
163            PropertyIdentifier::LANDING_CALL_CONTROL,
164            PropertyIdentifier::STATUS_FLAGS,
165            PropertyIdentifier::OUT_OF_SERVICE,
166            PropertyIdentifier::RELIABILITY,
167        ];
168        Cow::Borrowed(PROPS)
169    }
170}
171
172// ===========================================================================
173// EscalatorObject (type 58)
174// ===========================================================================
175
176/// BACnet Escalator object — represents an escalator.
177pub struct EscalatorObject {
178    oid: ObjectIdentifier,
179    name: String,
180    description: String,
181    /// Escalator mode (BACnetEscalatorMode, Clause 21); proprietary extensions
182    /// (Clause 23.1) are preserved as raw values.
183    escalator_mode: EscalatorMode,
184    /// Fault signal set (BACnetEscalatorFault, Clause 21).
185    fault_signals: Vec<EscalatorFault>,
186    /// Energy meter reading (Real).
187    energy_meter: f32,
188    /// Energy meter reference (stored as raw bytes).
189    energy_meter_ref: Vec<u8>,
190    /// Power mode (Boolean).
191    power_mode: bool,
192    /// Operation direction (BACnetEscalatorOperationDirection, Clause 21);
193    /// proprietary extensions (Clause 23.1) are preserved as raw values.
194    operation_direction: EscalatorOperationDirection,
195    /// Passenger alarm state (Boolean).
196    passenger_alarm: bool,
197    status_flags: StatusFlags,
198    out_of_service: bool,
199    reliability: u32,
200}
201
202impl EscalatorObject {
203    /// Create a new Escalator object with default values.
204    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
205        let oid = ObjectIdentifier::new(ObjectType::ESCALATOR, instance)?;
206        Ok(Self {
207            oid,
208            name: name.into(),
209            description: String::new(),
210            escalator_mode: EscalatorMode::UNKNOWN,
211            fault_signals: Vec::new(),
212            energy_meter: 0.0,
213            energy_meter_ref: Vec::new(),
214            power_mode: false,
215            operation_direction: EscalatorOperationDirection::UNKNOWN,
216            passenger_alarm: false,
217            status_flags: StatusFlags::empty(),
218            out_of_service: false,
219            reliability: 0,
220        })
221    }
222}
223
224impl BACnetObject for EscalatorObject {
225    fn object_identifier(&self) -> ObjectIdentifier {
226        self.oid
227    }
228
229    fn object_name(&self) -> &str {
230        &self.name
231    }
232
233    fn read_property(
234        &self,
235        property: PropertyIdentifier,
236        array_index: Option<u32>,
237    ) -> Result<PropertyValue, Error> {
238        if let Some(result) = read_common_properties!(self, property, array_index) {
239            return result;
240        }
241        match property {
242            p if p == PropertyIdentifier::OBJECT_TYPE => {
243                Ok(PropertyValue::Enumerated(ObjectType::ESCALATOR.to_raw()))
244            }
245            p if p == PropertyIdentifier::ESCALATOR_MODE => {
246                Ok(PropertyValue::Enumerated(self.escalator_mode.to_raw()))
247            }
248            p if p == PropertyIdentifier::FAULT_SIGNALS => {
249                let items: Vec<PropertyValue> = self
250                    .fault_signals
251                    .iter()
252                    .map(|v| PropertyValue::Enumerated(v.to_raw()))
253                    .collect();
254                Ok(PropertyValue::List(items))
255            }
256            p if p == PropertyIdentifier::ENERGY_METER => {
257                Ok(PropertyValue::Real(self.energy_meter))
258            }
259            p if p == PropertyIdentifier::ENERGY_METER_REF => {
260                Ok(PropertyValue::OctetString(self.energy_meter_ref.clone()))
261            }
262            p if p == PropertyIdentifier::POWER_MODE => Ok(PropertyValue::Boolean(self.power_mode)),
263            p if p == PropertyIdentifier::OPERATION_DIRECTION => {
264                Ok(PropertyValue::Enumerated(self.operation_direction.to_raw()))
265            }
266            p if p == PropertyIdentifier::PASSENGER_ALARM => {
267                Ok(PropertyValue::Boolean(self.passenger_alarm))
268            }
269            _ => Err(common::unknown_property_error()),
270        }
271    }
272
273    fn write_property(
274        &mut self,
275        property: PropertyIdentifier,
276        _array_index: Option<u32>,
277        value: PropertyValue,
278        _priority: Option<u8>,
279    ) -> Result<(), Error> {
280        if let Some(result) =
281            common::write_out_of_service(&mut self.out_of_service, property, &value)
282        {
283            return result;
284        }
285        if let Some(result) = common::write_description(&mut self.description, property, &value) {
286            return result;
287        }
288        match property {
289            p if p == PropertyIdentifier::POWER_MODE => {
290                if let PropertyValue::Boolean(v) = value {
291                    self.power_mode = v;
292                    Ok(())
293                } else {
294                    Err(common::invalid_data_type_error())
295                }
296            }
297            p if p == PropertyIdentifier::ESCALATOR_MODE => {
298                if let PropertyValue::Enumerated(v) = value {
299                    let named = EscalatorMode::ALL_NAMED
300                        .iter()
301                        .any(|&(_, value)| value.to_raw() == v);
302                    if !(named || (1024..=65535).contains(&v)) {
303                        return Err(common::value_out_of_range_error());
304                    }
305                    self.escalator_mode = EscalatorMode::from_raw(v);
306                    Ok(())
307                } else {
308                    Err(common::invalid_data_type_error())
309                }
310            }
311            p if p == PropertyIdentifier::OPERATION_DIRECTION => {
312                if let PropertyValue::Enumerated(v) = value {
313                    // Accept the six named values of the Clause 21
314                    // BACnetEscalatorOperationDirection production plus
315                    // proprietary extensions (Clause 23.1: 1024..=65535);
316                    // 6..=1023 is reserved and undefined in the 2020
317                    // production. Validate before mutating so a refused
318                    // write leaves the prior value intact.
319                    let named = EscalatorOperationDirection::ALL_NAMED
320                        .iter()
321                        .any(|&(_, value)| value.to_raw() == v);
322                    if !(named || (1024..=65535).contains(&v)) {
323                        return Err(common::value_out_of_range_error());
324                    }
325                    self.operation_direction = EscalatorOperationDirection::from_raw(v);
326                    Ok(())
327                } else {
328                    Err(common::invalid_data_type_error())
329                }
330            }
331            p if p == PropertyIdentifier::ENERGY_METER => {
332                if let PropertyValue::Real(v) = value {
333                    if !v.is_finite() {
334                        return Err(common::value_out_of_range_error());
335                    }
336                    self.energy_meter = v;
337                    Ok(())
338                } else {
339                    Err(common::invalid_data_type_error())
340                }
341            }
342            p if p == PropertyIdentifier::FAULT_SIGNALS => {
343                let values = match value {
344                    PropertyValue::Enumerated(v) => vec![PropertyValue::Enumerated(v)],
345                    PropertyValue::List(values) => values,
346                    _ => return Err(common::invalid_data_type_error()),
347                };
348                let mut faults = Vec::with_capacity(values.len());
349                let mut seen = HashSet::with_capacity(values.len());
350                for value in values {
351                    let PropertyValue::Enumerated(raw) = value else {
352                        return Err(common::invalid_data_type_error());
353                    };
354                    let named = EscalatorFault::ALL_NAMED
355                        .iter()
356                        .any(|&(_, value)| value.to_raw() == raw);
357                    if !(named || (1024..=65535).contains(&raw)) {
358                        return Err(common::value_out_of_range_error());
359                    }
360                    let fault = EscalatorFault::from_raw(raw);
361                    if !seen.insert(fault) {
362                        return Err(common::value_out_of_range_error());
363                    }
364                    faults.push(fault);
365                }
366                self.fault_signals = faults;
367                Ok(())
368            }
369            p if p == PropertyIdentifier::PASSENGER_ALARM => {
370                if let PropertyValue::Boolean(v) = value {
371                    self.passenger_alarm = v;
372                    Ok(())
373                } else {
374                    Err(common::invalid_data_type_error())
375                }
376            }
377            _ => Err(common::write_access_denied_error()),
378        }
379    }
380
381    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
382        static PROPS: &[PropertyIdentifier] = &[
383            PropertyIdentifier::OBJECT_IDENTIFIER,
384            PropertyIdentifier::OBJECT_NAME,
385            PropertyIdentifier::DESCRIPTION,
386            PropertyIdentifier::OBJECT_TYPE,
387            PropertyIdentifier::ESCALATOR_MODE,
388            PropertyIdentifier::FAULT_SIGNALS,
389            PropertyIdentifier::ENERGY_METER,
390            PropertyIdentifier::ENERGY_METER_REF,
391            PropertyIdentifier::POWER_MODE,
392            PropertyIdentifier::OPERATION_DIRECTION,
393            PropertyIdentifier::PASSENGER_ALARM,
394            PropertyIdentifier::STATUS_FLAGS,
395            PropertyIdentifier::OUT_OF_SERVICE,
396            PropertyIdentifier::RELIABILITY,
397        ];
398        Cow::Borrowed(PROPS)
399    }
400
401    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
402        matches!(
403            property,
404            PropertyIdentifier::DESCRIPTION
405                | PropertyIdentifier::OUT_OF_SERVICE
406                | PropertyIdentifier::POWER_MODE
407                | PropertyIdentifier::OPERATION_DIRECTION
408                | PropertyIdentifier::ESCALATOR_MODE
409                | PropertyIdentifier::ENERGY_METER
410                | PropertyIdentifier::FAULT_SIGNALS
411                | PropertyIdentifier::PASSENGER_ALARM
412        )
413    }
414}
415
416// ===========================================================================
417// LiftObject (type 59)
418// ===========================================================================
419
420/// BACnet Lift object — represents a single lift/elevator car.
421pub struct LiftObject {
422    oid: ObjectIdentifier,
423    name: String,
424    description: String,
425    /// Tracking value (Unsigned — current floor).
426    tracking_value: u64,
427    /// Car position (Unsigned).
428    car_position: u64,
429    /// Car moving direction (Enumerated: 0=unknown, 1=stopped, 2=up, 3=down).
430    car_moving_direction: u32,
431    /// Car door status (List of Unsigned).
432    car_door_status: Vec<u64>,
433    /// Car load as a percentage (Unsigned).
434    car_load: u64,
435    /// Number of landing doors (stored as count).
436    landing_doors: u64,
437    /// Floor text labels (List of String).
438    floor_text: Vec<String>,
439    /// Energy meter reading (Real).
440    energy_meter: f32,
441    status_flags: StatusFlags,
442    out_of_service: bool,
443    reliability: u32,
444}
445
446impl LiftObject {
447    /// Create a new Lift object with the given number of floors.
448    ///
449    /// Floor text is initialized to "Floor 1", "Floor 2", etc.
450    pub fn new(instance: u32, name: impl Into<String>, num_floors: usize) -> Result<Self, Error> {
451        let oid = ObjectIdentifier::new(ObjectType::LIFT, instance)?;
452        let floor_text = (1..=num_floors).map(|i| format!("Floor {i}")).collect();
453        Ok(Self {
454            oid,
455            name: name.into(),
456            description: String::new(),
457            tracking_value: 1,
458            car_position: 1,
459            car_moving_direction: 1, // stopped
460            car_door_status: Vec::new(),
461            car_load: 0,
462            landing_doors: num_floors as u64,
463            floor_text,
464            energy_meter: 0.0,
465            status_flags: StatusFlags::empty(),
466            out_of_service: false,
467            reliability: 0,
468        })
469    }
470}
471
472impl BACnetObject for LiftObject {
473    fn object_identifier(&self) -> ObjectIdentifier {
474        self.oid
475    }
476
477    fn object_name(&self) -> &str {
478        &self.name
479    }
480
481    fn read_property(
482        &self,
483        property: PropertyIdentifier,
484        array_index: Option<u32>,
485    ) -> Result<PropertyValue, Error> {
486        if let Some(result) = read_common_properties!(self, property, array_index) {
487            return result;
488        }
489        match property {
490            p if p == PropertyIdentifier::OBJECT_TYPE => {
491                Ok(PropertyValue::Enumerated(ObjectType::LIFT.to_raw()))
492            }
493            p if p == PropertyIdentifier::TRACKING_VALUE => {
494                Ok(PropertyValue::Unsigned(self.tracking_value))
495            }
496            p if p == PropertyIdentifier::CAR_POSITION => {
497                Ok(PropertyValue::Unsigned(self.car_position))
498            }
499            p if p == PropertyIdentifier::CAR_MOVING_DIRECTION => {
500                Ok(PropertyValue::Enumerated(self.car_moving_direction))
501            }
502            p if p == PropertyIdentifier::CAR_DOOR_STATUS => {
503                let items: Vec<PropertyValue> = self
504                    .car_door_status
505                    .iter()
506                    .map(|v| PropertyValue::Unsigned(*v))
507                    .collect();
508                Ok(PropertyValue::List(items))
509            }
510            p if p == PropertyIdentifier::CAR_LOAD => Ok(PropertyValue::Unsigned(self.car_load)),
511            p if p == PropertyIdentifier::LANDING_DOOR_STATUS => {
512                Ok(PropertyValue::Unsigned(self.landing_doors))
513            }
514            p if p == PropertyIdentifier::FLOOR_TEXT => {
515                let items: Vec<PropertyValue> = self
516                    .floor_text
517                    .iter()
518                    .map(|s| PropertyValue::CharacterString(s.clone()))
519                    .collect();
520                Ok(PropertyValue::List(items))
521            }
522            p if p == PropertyIdentifier::ENERGY_METER => {
523                Ok(PropertyValue::Real(self.energy_meter))
524            }
525            p if p == PropertyIdentifier::FLOOR_NUMBER => {
526                Ok(PropertyValue::Unsigned(self.tracking_value))
527            }
528            _ => Err(common::unknown_property_error()),
529        }
530    }
531
532    fn write_property(
533        &mut self,
534        property: PropertyIdentifier,
535        _array_index: Option<u32>,
536        value: PropertyValue,
537        _priority: Option<u8>,
538    ) -> Result<(), Error> {
539        if let Some(result) =
540            common::write_out_of_service(&mut self.out_of_service, property, &value)
541        {
542            return result;
543        }
544        if let Some(result) = common::write_description(&mut self.description, property, &value) {
545            return result;
546        }
547        match property {
548            p if p == PropertyIdentifier::TRACKING_VALUE => {
549                if let PropertyValue::Unsigned(v) = value {
550                    self.tracking_value = v;
551                    Ok(())
552                } else {
553                    Err(common::invalid_data_type_error())
554                }
555            }
556            p if p == PropertyIdentifier::CAR_POSITION => {
557                if let PropertyValue::Unsigned(v) = value {
558                    self.car_position = v;
559                    Ok(())
560                } else {
561                    Err(common::invalid_data_type_error())
562                }
563            }
564            p if p == PropertyIdentifier::CAR_MOVING_DIRECTION => {
565                if let PropertyValue::Enumerated(v) = value {
566                    if v > 3 {
567                        return Err(common::value_out_of_range_error());
568                    }
569                    self.car_moving_direction = v;
570                    Ok(())
571                } else {
572                    Err(common::invalid_data_type_error())
573                }
574            }
575            p if p == PropertyIdentifier::CAR_LOAD => {
576                if let PropertyValue::Unsigned(v) = value {
577                    if v > 100 {
578                        return Err(common::value_out_of_range_error());
579                    }
580                    self.car_load = v;
581                    Ok(())
582                } else {
583                    Err(common::invalid_data_type_error())
584                }
585            }
586            _ => Err(common::write_access_denied_error()),
587        }
588    }
589
590    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
591        static PROPS: &[PropertyIdentifier] = &[
592            PropertyIdentifier::OBJECT_IDENTIFIER,
593            PropertyIdentifier::OBJECT_NAME,
594            PropertyIdentifier::DESCRIPTION,
595            PropertyIdentifier::OBJECT_TYPE,
596            PropertyIdentifier::TRACKING_VALUE,
597            PropertyIdentifier::CAR_POSITION,
598            PropertyIdentifier::CAR_MOVING_DIRECTION,
599            PropertyIdentifier::CAR_DOOR_STATUS,
600            PropertyIdentifier::CAR_LOAD,
601            PropertyIdentifier::LANDING_DOOR_STATUS,
602            PropertyIdentifier::FLOOR_TEXT,
603            PropertyIdentifier::ENERGY_METER,
604            PropertyIdentifier::STATUS_FLAGS,
605            PropertyIdentifier::OUT_OF_SERVICE,
606            PropertyIdentifier::RELIABILITY,
607        ];
608        Cow::Borrowed(PROPS)
609    }
610}
611
612// ---------------------------------------------------------------------------
613// Tests
614// ---------------------------------------------------------------------------
615
616#[cfg(test)]
617mod tests;