Skip to main content

bacnet_objects/
traits.rs

1//! BACnetObject trait — the interface all BACnet objects implement.
2
3use std::any::Any;
4use std::borrow::Cow;
5use std::sync::Arc;
6use std::time::Duration;
7
8use bacnet_types::constructed::BACnetLogRecord;
9use bacnet_types::enums::{
10    ErrorClass, ErrorCode, EventState, LifeSafetyOperation, PropertyIdentifier,
11};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue};
14
15use crate::audit::{AuditLogNotificationSink, AuditLogStorage};
16use crate::clock::ClockReader;
17use crate::event::{
18    EnrollmentSummaryCapability, EventStateChange, EventTransitionCommit,
19    EventTransitionCommitError, TransitionOutcome,
20};
21use crate::event_enrollment::{
22    EventEnrollmentEvalState, EventEnrollmentMonitoredSource, EventEnrollmentReliabilityCommit,
23};
24use crate::file::{FileConfiguration, FileStorage};
25use crate::log_buffer::LogRecordIdentity;
26
27/// Process-local monotonic time source used by internal object lifecycles.
28#[doc(hidden)]
29pub type MonotonicClock = dyn Fn() -> Duration + Send + Sync;
30
31mod defaults;
32use defaults::{array_property_default, historical_writable_default};
33
34/// Object-owned snapshot state retained for compatibility and local use.
35///
36/// The bundled server no longer invokes these tokens from Service 16:
37/// WritePropertyMultiple retains its successful prefix as required by Clause
38/// 15.10. Existing object implementations and downstream users may still use
39/// the hooks directly to preserve state hidden by property readback, including
40/// event resets, fallback-backed values, destructive writes, and derived state.
41#[doc(hidden)]
42pub struct WritePropertyRollback(Box<dyn Any + Send + Sync>);
43
44/// Result of applying a LifeSafetyOperation to an object.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum LifeSafetyOperationEffect {
47    /// The operation changed object state.
48    Applied,
49    /// The requested idempotent state was already present.
50    AlreadyApplied,
51}
52
53/// Detailed result of applying a `LifeSafetyOperation` to object-owned state.
54///
55/// `changed_properties` is ordered by the object's stable reporting order and
56/// contains each property at most once. Custom objects that implement only
57/// [`BACnetObject::apply_life_safety_operation`] remain source-compatible: the
58/// default detailed hook delegates to that method and reports no properties
59/// because the trait cannot truthfully infer their object-private mutations.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct LifeSafetyOperationOutcome {
62    /// Existing coarse operation result.
63    pub effect: LifeSafetyOperationEffect,
64    /// Exact properties whose committed readback changed.
65    pub changed_properties: Vec<PropertyIdentifier>,
66}
67
68/// Result of one object-owned reliability evaluation pass.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum ReliabilityEvaluation {
71    /// The object made no reliability-related mutation.
72    Unchanged,
73    /// The object successfully mutated its `Reliability` value.
74    Changed {
75        /// Reliability before the successful mutation.
76        old_reliability: u32,
77        /// Reliability after the successful mutation.
78        new_reliability: u32,
79    },
80}
81
82impl WritePropertyRollback {
83    /// Wrap object-private rollback state.
84    #[doc(hidden)]
85    pub fn new<T: Any + Send + Sync>(state: T) -> Self {
86        Self(Box::new(state))
87    }
88
89    /// Recover object-private rollback state.
90    #[doc(hidden)]
91    pub fn downcast<T: Any + Send + Sync>(self) -> Result<T, Error> {
92        self.0.downcast::<T>().map(|state| *state).map_err(|_| {
93            Error::Encoding("object received an incompatible write rollback token".into())
94        })
95    }
96}
97
98/// The core trait for all BACnet objects.
99///
100/// Implementors represent a single BACnet object (Device, AnalogInput, etc.)
101/// and provide read/write access to their properties.
102pub trait BACnetObject: Send + Sync {
103    /// The object's identifier (type + instance).
104    fn object_identifier(&self) -> ObjectIdentifier;
105
106    /// The object's name.
107    fn object_name(&self) -> &str;
108
109    /// Read a property value.
110    fn read_property(
111        &self,
112        property: PropertyIdentifier,
113        array_index: Option<u32>,
114    ) -> Result<PropertyValue, Error>;
115
116    /// Write a property value.
117    ///
118    /// Returning `Err` MUST leave the object unchanged. WritePropertyMultiple
119    /// retains earlier successful writes and cannot undo a mutation made by the
120    /// currently failing write, including for a write-only property.
121    fn write_property(
122        &mut self,
123        property: PropertyIdentifier,
124        array_index: Option<u32>,
125        value: PropertyValue,
126        priority: Option<u8>,
127    ) -> Result<(), Error>;
128
129    /// Return canonical metadata for this object's effective property rows.
130    ///
131    /// Migrated implementations return every supported standard row for the
132    /// current instance, including `PROPERTY_LIST`. Borrowed rows cover static
133    /// or object-owned metadata; owned rows support dynamically assembled
134    /// per-instance sets. An empty borrowed default marks an object as unmigrated.
135    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
136        Cow::Borrowed(&[])
137    }
138
139    /// List all properties this object supports in the legacy projection.
140    ///
141    /// For migrated objects this includes Object_Identifier, Object_Name, and
142    /// Object_Type but omits Property_List. Reading the BACnet Property_List
143    /// property applies the additional wire-level universal-property filter.
144    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]>;
145
146    /// Bind or remove the database's shared wall-clock reader.
147    ///
148    /// Objects that do not expose clock-derived state ignore this internal
149    /// lifecycle hook.
150    #[doc(hidden)]
151    fn bind_clock_internal(&mut self, _clock: Option<Arc<dyn ClockReader>>) {}
152
153    /// Advance object-owned operations that use monotonic elapsed time.
154    ///
155    /// The server calls this internal hook from a dedicated lifecycle task.
156    /// `true` means readable state changed and generic COV processing is
157    /// required. The default is a source-compatible no-op for downstream
158    /// object implementations.
159    #[doc(hidden)]
160    fn advance_time_internal(&mut self, _elapsed: Duration) -> bool {
161        false
162    }
163
164    /// Bind the process-local monotonic source used when operations arm.
165    #[doc(hidden)]
166    fn bind_monotonic_clock_internal(&mut self, _clock: Option<Arc<MonotonicClock>>) {}
167
168    /// Advance operations to an absolute process-local monotonic instant.
169    #[doc(hidden)]
170    fn advance_monotonic_time_internal(&mut self, _now: Duration) -> bool {
171        false
172    }
173
174    /// Return the next absolute process-local operation deadline, if any.
175    #[doc(hidden)]
176    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
177        None
178    }
179
180    /// Freeze COV-readable state while the object's mutation lock is held.
181    #[doc(hidden)]
182    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
183        None
184    }
185
186    /// Return the retained logical blink-request observation, when modeled.
187    ///
188    /// This is an internal conformance-test channel, not a BACnet property,
189    /// host callback, or physical-output claim.
190    #[doc(hidden)]
191    fn binary_lighting_blink_count_internal(&self) -> u64 {
192        0
193    }
194
195    /// Whether `write_property` accepts `property` for this object.
196    ///
197    /// PICS generation and runtime dispatch MUST consult this (or
198    /// `write_property` itself) rather than a separate heuristic, so the PICS
199    /// writable flags cannot drift from the actual write routes. The default
200    /// reproduces the historical PICS heuristic (see
201    /// [`historical_writable_default`]) so unmigrated object types keep their
202    /// current PICS output. Object implementations override to mirror their
203    /// real `write_property` arms exactly.
204    ///
205    /// Universal read-only properties (`OBJECT_IDENTIFIER`, `OBJECT_TYPE`,
206    /// `PROPERTY_LIST`, `STATUS_FLAGS`) are always non-writable and are
207    /// excluded by the default; overrides should preserve that invariant.
208    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
209        let metadata = self.property_metadata();
210        if metadata.is_empty() {
211            historical_writable_default(self.object_identifier().object_type(), property)
212        } else {
213            crate::property_metadata::is_writable_in_metadata(metadata.as_ref(), property)
214        }
215    }
216
217    /// Whether `property` accepts an array index on this object.
218    ///
219    /// Per Clause 12.1.5.1, only BACnetARRAY (and BACnetARRAY of BACnetLIST)
220    /// properties accept an array index; Clause 12.1.5.2 makes ReadRange the
221    /// only positional access to a BACnetLIST. The RP/RPM/WP/WPM service
222    /// handlers gate the request's array index on this query and reject a
223    /// supplied index on a non-array property with PROPERTY /
224    /// PROPERTY_IS_NOT_AN_ARRAY (Clause 15.5.1.3, Clause 15.9.1.3).
225    ///
226    /// The default reproduces the standard's classification (see
227    /// [`array_property_default`]): identifier-stable arrays are admitted
228    /// without consulting the object type, the identifiers whose datatype
229    /// changes with the object type (ALARM_VALUES / FAULT_VALUES,
230    /// LIST_OF_OBJECT_PROPERTY_REFERENCES, PRESENT_VALUE) classify by
231    /// `object_identifier().object_type()`, and everything else — scalars and
232    /// BACnetLIST properties — rejects the index. Object implementations with
233    /// vendor or per-instance array properties override.
234    fn is_array_property(&self, property: PropertyIdentifier) -> bool {
235        array_property_default(self.object_identifier().object_type(), property)
236    }
237
238    /// Capture compatibility state that property readback cannot preserve.
239    ///
240    /// The default returns `None`. Service 16 no longer calls this hook because
241    /// WritePropertyMultiple retains successful prefix writes. Implementations
242    /// and downstream callers may still use a token directly for object-local
243    /// snapshot/restore flows involving destructive or fallback-backed state.
244    /// Returning `None` MUST leave the object unchanged.
245    #[doc(hidden)]
246    fn capture_write_property_rollback(
247        &mut self,
248        _property: PropertyIdentifier,
249        _value: &PropertyValue,
250    ) -> Option<WritePropertyRollback> {
251        None
252    }
253
254    /// Restore a compatibility token returned by
255    /// [`capture_write_property_rollback`](Self::capture_write_property_rollback).
256    ///
257    /// The bundled server's Service 16 path does not invoke this hook.
258    #[doc(hidden)]
259    fn restore_write_property_rollback(
260        &mut self,
261        _rollback: WritePropertyRollback,
262    ) -> Result<(), Error> {
263        Err(Error::Encoding(
264            "object does not support this write rollback token".into(),
265        ))
266    }
267
268    /// Whether this object type can be created at runtime via CreateObject.
269    ///
270    /// Default `false`; override `true` only for types the network factory
271    /// (`handle_create_object`) actually constructs, so PICS createability
272    /// matches the runtime factory with no separate list to drift.
273    fn is_createable(&self) -> bool {
274        false
275    }
276
277    /// Whether this object type can be deleted at runtime via DeleteObject.
278    ///
279    /// Default `true`; override `false` on object types that are not
280    /// deleteable (e.g. `Device`, `NetworkPort`).
281    fn is_deleteable(&self) -> bool {
282        true
283    }
284
285    /// List the REQUIRED properties for this object type.
286    ///
287    /// Migrated objects derive this set from canonical `R` and `W` rows,
288    /// including Property_List. Unmigrated objects retain the historical four
289    /// universal properties. Service-specific consumers may exclude
290    /// Property_List where their protocol contract requires it.
291    fn required_properties(&self) -> Cow<'static, [PropertyIdentifier]> {
292        let metadata = self.property_metadata();
293        if !metadata.is_empty() {
294            return crate::property_metadata::required_properties_from_metadata(metadata.as_ref());
295        }
296        static UNIVERSAL: [PropertyIdentifier; 4] = [
297            PropertyIdentifier::OBJECT_IDENTIFIER,
298            PropertyIdentifier::OBJECT_NAME,
299            PropertyIdentifier::OBJECT_TYPE,
300            PropertyIdentifier::PROPERTY_LIST,
301        ];
302        Cow::Borrowed(&UNIVERSAL)
303    }
304
305    /// Whether this object type supports COV notifications.
306    ///
307    /// Override to return `true` for object types that can generate COV
308    /// notifications (analog, binary, multi-state I/O/V). Default is `false`.
309    fn supports_cov(&self) -> bool {
310        false
311    }
312
313    /// Take pending local target work from a Staging object.
314    ///
315    /// The default keeps every non-Staging object source-compatible. The
316    /// bundled server calls this only while it owns the database mutation
317    /// guard, then performs the returned writes after releasing that guard.
318    #[doc(hidden)]
319    fn take_staging_write_plan_internal(&mut self) -> Option<crate::staging::StagingWritePlan> {
320        None
321    }
322
323    /// Return the current Staging transition generation, if applicable.
324    #[doc(hidden)]
325    fn staging_generation_internal(&self) -> Option<u64> {
326        None
327    }
328
329    /// Commit one guarded Staging plan's success or failure to Reliability.
330    ///
331    /// Returns whether readable source state changed. Implementations ignore a
332    /// stale generation so older work cannot fault a newer transition.
333    #[doc(hidden)]
334    fn complete_staging_write_plan_internal(&mut self, _generation: u64, _success: bool) -> bool {
335        false
336    }
337
338    /// Return this object's GetEnrollmentSummary event capability.
339    ///
340    /// The default opts custom and downstream objects out. Implementations opt
341    /// in only when they own an actual configured or implied event algorithm
342    /// and shared committed-transition history.
343    #[doc(hidden)]
344    fn enrollment_summary_capability_internal(&self) -> Option<EnrollmentSummaryCapability> {
345        None
346    }
347
348    /// Whether a readable property supports property-specific COV.
349    ///
350    /// The default preserves the existing behavior of every other COV-capable
351    /// object family while enforcing the bounded standardized Life Safety
352    /// surface for source-compatible custom Point and Zone implementations.
353    fn supports_cov_property(&self, property: PropertyIdentifier) -> bool {
354        use bacnet_types::enums::ObjectType;
355
356        match self.object_identifier().object_type() {
357            ObjectType::LIFE_SAFETY_POINT => matches!(
358                property,
359                PropertyIdentifier::PRESENT_VALUE
360                    | PropertyIdentifier::STATUS_FLAGS
361                    | PropertyIdentifier::TRACKING_VALUE
362                    | PropertyIdentifier::SILENCED
363                    | PropertyIdentifier::OPERATION_EXPECTED
364            ),
365            ObjectType::LIFE_SAFETY_ZONE => matches!(
366                property,
367                PropertyIdentifier::PRESENT_VALUE
368                    | PropertyIdentifier::STATUS_FLAGS
369                    | PropertyIdentifier::SILENCED
370                    | PropertyIdentifier::OPERATION_EXPECTED
371            ),
372            _ => self.supports_cov(),
373        }
374    }
375
376    /// COV increment for this object (analog objects only).
377    ///
378    /// Returns `Some(increment)` for objects that use COV_Increment filtering
379    /// (e.g., AnalogInput, AnalogOutput, AnalogValue). A notification fires only
380    /// when `|current_value - last_notified_value| >= increment`.
381    ///
382    /// Returns `None` for objects that notify on any state change (binary, multi-state).
383    fn cov_increment(&self) -> Option<f32> {
384        None
385    }
386
387    /// Set the OVERRIDDEN bit in StatusFlags.
388    ///
389    /// For software-only objects this is always FALSE per spec. Hardware
390    /// integrations can override to set TRUE when present_value is overridden
391    /// by physical means (e.g., a manual switch on an output).
392    fn set_overridden(&mut self, _overridden: bool) {}
393
394    /// Evaluate intrinsic reporting after a present_value change.
395    ///
396    /// This is the per-write entry point: it seeds (or cancels) a pending
397    /// delayed transition and fires immediately only when `Time_Delay == 0`.
398    /// It never advances the `Time_Delay` countdown — repeated writes to the
399    /// same value do not shorten the delay (per ASHRAE 135-2020 §13.2.4 the
400    /// countdown advances once per elapsed second via
401    /// [`tick_intrinsic_reporting`](Self::tick_intrinsic_reporting)).
402    ///
403    /// Returns `Some(TransitionOutcome)` whenever a transition is ready to be
404    /// committed, or
405    /// `None` when none did (no change, delay seeded, or the object does not
406    /// support intrinsic reporting). A cleared `Event_Enable` bit sets the
407    /// outcome's `distribute` flag to false rather than withholding the
408    /// transition. Built-in object families leave `Event_State`,
409    /// `Acked_Transitions`, event history, and fire-ready detector state
410    /// unchanged until [`commit_event_transition_internal`](Self::commit_event_transition_internal)
411    /// succeeds. Clause 13.2.2.1.4's transition actions run either way, and
412    /// `Event_Enable` disables only external distribution, downstream in the
413    /// notification-distribution process (Clause 13.2.5).
414    fn evaluate_intrinsic_reporting(&mut self) -> Option<TransitionOutcome> {
415        None
416    }
417
418    /// Advance the `Time_Delay` countdown for a pending transition.
419    ///
420    /// Called by the server's one-second intrinsic-reporting task. Fires the
421    /// pending transition when its delay elapses this tick, cancels it if the
422    /// triggering condition reverted, and returns `Some(TransitionOutcome)`
423    /// when a transition is ready. A fire-ready built-in proposal remains
424    /// retryable until the commit hook succeeds. As with
425    /// [`evaluate_intrinsic_reporting`](Self::evaluate_intrinsic_reporting),
426    /// `Event_Enable` is reported via `distribute`, not by returning `None`.
427    /// Objects without a delayed transition return `None`.
428    fn tick_intrinsic_reporting(&mut self) -> Option<TransitionOutcome> {
429        None
430    }
431
432    /// Whether intrinsic-reporting outcomes require the atomic commit hook.
433    ///
434    /// The default preserves the legacy contract used by downstream objects
435    /// and [`crate::impl_intrinsic_reporting!`]: evaluation mutates detector
436    /// state immediately, so the server must distribute its outcome without
437    /// attempting another commit. Built-in proposal-based objects override
438    /// this to return `true`; their outcomes are uncommitted and must be
439    /// rejected unless [`commit_event_transition_internal`](Self::commit_event_transition_internal)
440    /// succeeds.
441    #[doc(hidden)]
442    fn intrinsic_reporting_requires_atomic_commit(&self) -> bool {
443        false
444    }
445
446    /// Atomically commit all object-owned state for one event transition.
447    ///
448    /// The caller supplies an exact state change, its transition coordinate,
449    /// the resolved Notification Class `Ack_Required` value, a typed
450    /// timestamp, and an optional message. Implementations must validate the
451    /// coordinate and source state before changing `Event_State`,
452    /// `Acked_Transitions`, `Event_Time_Stamps`, or stored message state, and
453    /// must leave every value unchanged on error. A successful built-in
454    /// implementation also finalizes the detector's pending and fault-edge
455    /// state; a failed commit leaves that state retryable.
456    ///
457    /// This internal channel is deliberately separate from network property
458    /// writes and notification distribution. The default fails closed so an
459    /// object family participates only after it can lend all required state to
460    /// the shared commit kernel.
461    #[doc(hidden)]
462    fn commit_event_transition_internal(
463        &mut self,
464        _commit: EventTransitionCommit,
465    ) -> Result<(), EventTransitionCommitError> {
466        Err(EventTransitionCommitError::Unsupported)
467    }
468
469    /// Atomically commit Event Enrollment Reliability and transition state.
470    ///
471    /// This Event Enrollment-specific channel joins `Reliability` to the
472    /// existing atomic `Event_State`, `Acked_Transitions`, and
473    /// `Event_Time_Stamps` commit. Implementations must stage every supplied
474    /// value before assigning object-owned fields and leave them unchanged on
475    /// error. The default fails closed for custom objects that have not adopted
476    /// the stronger contract.
477    #[doc(hidden)]
478    fn commit_event_enrollment_reliability_internal(
479        &mut self,
480        _commit: EventEnrollmentReliabilityCommit,
481    ) -> Result<(), EventTransitionCommitError> {
482        Err(EventTransitionCommitError::Unsupported)
483    }
484
485    /// Evaluate this object's schedule for the given time.
486    ///
487    /// Returns `Some((new_value, refs))` if the present value changed, where `refs`
488    /// is the list of (object_identifier, property_identifier) pairs to write to.
489    /// Only meaningful for Schedule objects; default returns `None`.
490    fn tick_schedule(
491        &mut self,
492        _day_of_week: u8,
493        _hour: u8,
494        _minute: u8,
495    ) -> Option<(PropertyValue, Vec<(ObjectIdentifier, u32)>)> {
496        None
497    }
498
499    /// Acknowledge an alarm transition. Sets the corresponding bit in acked_transitions.
500    /// Returns Ok(()) if the object supports event detection, Err otherwise.
501    fn acknowledge_alarm(&mut self, _transition_bit: u8) -> Result<(), bacnet_types::error::Error> {
502        Err(bacnet_types::error::Error::Protocol {
503            class: bacnet_types::enums::ErrorClass::OBJECT.to_raw() as u32,
504            code: bacnet_types::enums::ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw()
505                as u32,
506        })
507    }
508
509    /// Correlate an acknowledgment with the latest committed transition.
510    #[doc(hidden)]
511    fn acknowledge_alarm_correlated_internal(
512        &mut self,
513        _event_state: EventState,
514        _timestamp: &BACnetTimeStamp,
515    ) -> Result<(), Error> {
516        Err(Error::Protocol {
517            class: ErrorClass::OBJECT.to_raw() as u32,
518            code: ErrorCode::NO_ALARM_CONFIGURED.to_raw() as u32,
519        })
520    }
521
522    /// Correlate an acknowledgment and report exact object-owned history.
523    ///
524    /// The default delegates to the source-compatible coarse hook and returns
525    /// no notification context. Custom objects that implement only the coarse
526    /// hook therefore retain their accepted-acknowledgment behavior without the
527    /// bundled server guessing historical states for an ACK notification.
528    #[doc(hidden)]
529    fn acknowledge_alarm_correlated_detailed_internal(
530        &mut self,
531        event_state: EventState,
532        timestamp: &BACnetTimeStamp,
533    ) -> Result<Option<EventStateChange>, Error> {
534        self.acknowledge_alarm_correlated_internal(event_state, timestamp)
535            .map(|()| None)
536    }
537
538    /// Apply a LifeSafetyOperation atomically to this object.
539    ///
540    /// Implementations must leave the object unchanged when returning `Err`.
541    /// They run synchronously under the object-database write lock and must be
542    /// fast, nonblocking, and panic-free. External or irreversible actuation
543    /// also needs an application-owned idempotency/replay contract. The default
544    /// reports that the object does not support this service.
545    fn apply_life_safety_operation(
546        &mut self,
547        _operation: LifeSafetyOperation,
548    ) -> Result<LifeSafetyOperationEffect, Error> {
549        Err(Error::Protocol {
550            class: ErrorClass::OBJECT.to_raw() as u32,
551            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
552        })
553    }
554
555    /// Apply a LifeSafetyOperation and report exact known property deltas.
556    ///
557    /// The default delegates to the source-compatible coarse hook and reports
558    /// no known properties rather than guessing about custom object state.
559    fn apply_life_safety_operation_detailed(
560        &mut self,
561        operation: LifeSafetyOperation,
562    ) -> Result<LifeSafetyOperationOutcome, Error> {
563        self.apply_life_safety_operation(operation)
564            .map(|effect| LifeSafetyOperationOutcome {
565                effect,
566                changed_properties: Vec::new(),
567            })
568    }
569
570    /// Set the next LifeSafetyOperation expected by trusted local logic.
571    ///
572    /// This is an application-facing state channel, not a network property
573    /// write. The default reports that the object does not support the state.
574    fn set_life_safety_operation_expected_internal(
575        &mut self,
576        _operation: LifeSafetyOperation,
577    ) -> Result<(), Error> {
578        Err(Error::Protocol {
579            class: ErrorClass::OBJECT.to_raw() as u32,
580            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
581        })
582    }
583
584    /// Apply an internally-detected `Event_State` transition.
585    ///
586    /// This is the **internal** lifecycle path for the algorithmically-derived
587    /// `Event_State` on objects such as Event Enrollment (ASHRAE 135-2020
588    /// Clause 12.12). It is deliberately distinct from the network
589    /// [`write_property`](Self::write_property) route: `Event_State` is
590    /// read-only over the network, so network writes are rejected while the
591    /// server's evaluator reaches the field through this method. Objects
592    /// without an algorithmic `Event_State` return
593    /// `OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED`.
594    ///
595    /// The **default** returns `Err`, so objects without an algorithmic
596    /// `Event_State` opt out. Objects that do model one (e.g.
597    /// `EventEnrollmentObject`) override this to store the value verbatim:
598    /// the only caller is a trusted internal evaluator that passes a modeled
599    /// [`EventState`], mirroring the inherent `set_event_state` builder and
600    /// the existing read arm. Network-facing validation — rejecting all
601    /// `Event_State` writes — lives in [`write_property`](Self::write_property),
602    /// not here. Implementations must leave `Event_State` unchanged when they
603    /// return `Err`.
604    fn set_event_state_internal(&mut self, _state: EventState) -> Result<(), Error> {
605        Err(Error::Protocol {
606            class: ErrorClass::OBJECT.to_raw() as u32,
607            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
608        })
609    }
610
611    /// Snapshot this object's Event Enrollment evaluation state, if it models one.
612    ///
613    /// This is the read half of the internal channel the server's Event
614    /// Enrollment evaluator uses to persist per-enrollment algorithm state
615    /// across evaluation cycles: the pending (delayed) transition countdown,
616    /// the CHANGE_OF_VALUE detection baseline (Clause 13.3.3: "the value of
617    /// the monitored value when a transition to NORMAL is indicated"), and
618    /// the value that caused the last transition to OFFNORMAL (Clause 13.3.2
619    /// condition (c)). Like [`set_event_state_internal`](Self::set_event_state_internal)
620    /// it deliberately bypasses the network property model: none of the three
621    /// slots is a BACnet property, and 135-2020 assigns their initialization
622    /// to local matters.
623    ///
624    /// The default returns `None` — objects without algorithmic event
625    /// detection carry no such state, and the evaluator treats `None` as an
626    /// empty state it cannot write back (delay honoring and the COV baseline
627    /// then stay unavailable, matching this crate's pre-delay behavior).
628    fn enrollment_eval_state_internal(&self) -> Option<EventEnrollmentEvalState> {
629        None
630    }
631
632    /// Store this object's Event Enrollment evaluation state.
633    ///
634    /// The write half of [`enrollment_eval_state_internal`](Self::enrollment_eval_state_internal).
635    /// The only caller is the trusted server evaluator, passing a state it
636    /// derived from a prior snapshot plus the current cycle's evaluation.
637    /// Implementations enforce the Clause 13.2.2.1 invariant by construction:
638    /// while `Event_Detection_Enable` is FALSE "no transitions shall occur",
639    /// so a write arriving then is refused rather than queued (and the
640    /// detection-disable reset has already cleared the fields).
641    ///
642    /// The **default** returns `Err`, so objects without enrollment evaluation
643    /// state opt out and the evaluator's write-back is dropped, never stored
644    /// into an object that does not model it.
645    fn set_enrollment_eval_state_internal(
646        &mut self,
647        _state: EventEnrollmentEvalState,
648    ) -> Result<(), Error> {
649        Err(Error::Protocol {
650            class: ErrorClass::OBJECT.to_raw() as u32,
651            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
652        })
653    }
654
655    /// Snapshot the monitored source that owns Event Enrollment private state.
656    ///
657    /// The outer `Option` indicates whether the object supports this channel;
658    /// the inner `Option` is empty before a source has been established.
659    /// The server stores source ownership in its object database when an
660    /// object implements evaluation state but leaves this channel unsupported.
661    fn enrollment_eval_source_internal(&self) -> Option<Option<EventEnrollmentMonitoredSource>> {
662        None
663    }
664
665    /// Store or clear the monitored source that owns Event Enrollment state.
666    fn set_enrollment_eval_source_internal(
667        &mut self,
668        _source: Option<EventEnrollmentMonitoredSource>,
669    ) -> Result<(), Error> {
670        Err(Error::Protocol {
671            class: ErrorClass::OBJECT.to_raw() as u32,
672            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
673        })
674    }
675
676    /// Set or clear one `Acked_Transitions` bit on a received event-state
677    /// transition.
678    ///
679    /// Implements the alarm-acknowledgment half of Clause 13.2.2.1.4's fourth
680    /// transition action ("indicate the transition to the Alarm-Acknowledgment
681    /// process"), per Clause 13.2.3: "When an event state transition is
682    /// received, the corresponding bit in Acked_Transitions is either set or
683    /// cleared. If the corresponding bit in Ack_Required is set, then the bit
684    /// in Acked_Transitions is cleared, otherwise it is set." The caller (the
685    /// server evaluator) resolves `Ack_Required` from the referenced
686    /// Notification Class object and passes the outcome as `acknowledged`;
687    /// this method performs only the bit maintenance.
688    ///
689    /// `transition_bit` is the transition direction's bit mask in
690    /// `Acked_Transitions`' internal bit0-first form (`0x01` TO_OFFNORMAL,
691    /// `0x02` TO_FAULT, `0x04` TO_NORMAL). The set half overlaps the
692    /// network-reachable [`acknowledge_alarm`](Self::acknowledge_alarm), which
693    /// also ORs the bit in per Clause 13.2.3's acknowledgment-indication
694    /// paragraph; the clear half has no network route by design (a property
695    /// write could fabricate or erase acknowledgments — see the
696    /// `write_generic_event_properties!` denial comment).
697    ///
698    /// The **default** returns `Err`, so objects without an algorithmic
699    /// `Acked_Transitions` opt out.
700    fn set_acked_transitions_internal(
701        &mut self,
702        _transition_bit: u8,
703        _acknowledged: bool,
704    ) -> Result<(), Error> {
705        Err(Error::Protocol {
706            class: ErrorClass::OBJECT.to_raw() as u32,
707            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
708        })
709    }
710
711    /// Evaluate and, when necessary, mutate this object's `Reliability`.
712    ///
713    /// Reliability evaluation is object-owned: an implementation decides
714    /// whether and how its internal conditions affect `Reliability`, performs
715    /// the mutation itself, and returns [`ReliabilityEvaluation::Changed`] only
716    /// after that mutation succeeds. [`ReliabilityEvaluation::Unchanged`] means
717    /// the object made no mutation. Returning `Err` MUST leave all object state
718    /// unchanged.
719    ///
720    /// The default opts out without changing state, preserving source
721    /// compatibility for object implementations that do not own a reliability
722    /// evaluation algorithm.
723    fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
724        Ok(ReliabilityEvaluation::Unchanged)
725    }
726
727    /// Whether periodic object-owned reliability evaluation is currently
728    /// inhibited.
729    ///
730    /// The default is FALSE so existing and downstream object implementations
731    /// remain source-compatible. Objects that elect the optional
732    /// Reliability_Evaluation_Inhibit property override this internal
733    /// predicate; reporting of an already-applied Reliability transition is
734    /// intentionally unaffected.
735    #[doc(hidden)]
736    fn reliability_evaluation_inhibited_internal(&self) -> bool {
737        false
738    }
739
740    /// Apply an internally-derived `Reliability` value.
741    ///
742    /// This is the **internal** reliability-evaluation path, distinct from the
743    /// network [`write_property`](Self::write_property) route. Implementations
744    /// enforce symmetric ownership: clients may write while `Out_Of_Service`
745    /// is TRUE, and internal evaluation may write while it is FALSE. ASHRAE
746    /// 135-2020 Clause 3.2 defines reliability evaluation as "the process by
747    /// which an object determines its reliability and thus the value to set
748    /// into its Reliability property."
749    ///
750    /// The default rejects the operation, so object types without an internal
751    /// reliability-evaluation process remain unaffected.
752    fn set_reliability_internal(&mut self, _reliability: u32) -> Result<(), Error> {
753        Err(Error::Protocol {
754            class: ErrorClass::OBJECT.to_raw() as u32,
755            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
756        })
757    }
758
759    /// Apply the logical `Present_Value` supplied by an Input's application.
760    ///
761    /// This narrow internal hook is distinct from the network
762    /// [`write_property`](Self::write_property) route. Built-in Analog Input,
763    /// Binary Input, and Multi-state Input objects opt in. Their implementations
764    /// accept application updates only while in service; rejecting them while
765    /// `Out_Of_Service` is TRUE is a local ownership policy that protects the
766    /// client's simulation value, not a requirement imposed by the Standard.
767    ///
768    /// The default fails closed so commandable and other object families do not
769    /// acquire privileged `Present_Value` write authority through this hook.
770    fn set_present_value_internal(&mut self, _value: PropertyValue) -> Result<(), Error> {
771        Err(Error::Protocol {
772            class: ErrorClass::OBJECT.to_raw() as u32,
773            code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
774        })
775    }
776
777    /// Borrow this object's Audit Log query storage, if it has any.
778    ///
779    /// This read-only, type-erased channel lets the bundled server execute an
780    /// AuditLogQuery against an object's already-loaded retained records. It
781    /// deliberately does not expose persistence, mutation, or object
782    /// downcasting. The **default** returns `None`; an Audit Log object that
783    /// does not opt in is reported as SERVICES /
784    /// OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.
785    fn audit_log_storage_internal(&self) -> Option<&dyn AuditLogStorage> {
786        None
787    }
788
789    /// Mutably borrow this object's Audit notification receiver capability.
790    ///
791    /// The default opts out. Implementations own their persistence transaction
792    /// and must leave memory unchanged when the batch cannot be committed.
793    fn audit_log_notification_sink_internal(
794        &mut self,
795    ) -> Option<&mut dyn AuditLogNotificationSink> {
796        None
797    }
798
799    /// Borrow this object's trusted local File configuration capability.
800    ///
801    /// The default returns `None`. The built-in [`crate::file::FileObject`]
802    /// opts in so bindings can configure its pending payload, access method,
803    /// and growth limits without general object downcasting or a parallel
804    /// state cache.
805    #[doc(hidden)]
806    fn file_configuration_internal(&self) -> Option<&dyn FileConfiguration> {
807        None
808    }
809
810    /// Mutably borrow this object's trusted local File configuration capability.
811    ///
812    /// The default returns `None`; payload mutations remain object-owned and
813    /// must preserve the same accounting and metadata behavior as the built-in
814    /// File setters.
815    #[doc(hidden)]
816    fn file_configuration_internal_mut(&mut self) -> Option<&mut dyn FileConfiguration> {
817        None
818    }
819
820    /// Borrow this object's File storage, if it has any.
821    ///
822    /// This is the read half of the **internal** channel the server's
823    /// AtomicReadFile handler uses to reach file contents. Like
824    /// [`set_event_state_internal`](Self::set_event_state_internal) it
825    /// bypasses the property model on purpose: Table 12-16 (ASHRAE 135-2020
826    /// Clause 12.13) defines no File Data property, so file contents are
827    /// reachable only through the Clause 14 File Access Services.
828    ///
829    /// The **default** returns `None`, so object types without a file opt
830    /// out; the server reports `None` on a File-typed object as SERVICES /
831    /// FILE_ACCESS_DENIED (Clause 18: "a file that is currently locked or
832    /// otherwise not accessible") rather than reading it as empty.
833    /// Applications backing a File object with their own storage — a disk
834    /// file, a firmware partition — implement [`FileStorage`] and return
835    /// `Some`.
836    fn file_storage_internal(&self) -> Option<&dyn FileStorage> {
837        None
838    }
839
840    /// Mutably borrow this object's File storage, if it has any.
841    ///
842    /// The write half of
843    /// [`file_storage_internal`](Self::file_storage_internal), used by the
844    /// AtomicWriteFile handler after the read-only and access-method gates
845    /// have passed. The **default** returns `None`.
846    fn file_storage_internal_mut(&mut self) -> Option<&mut dyn FileStorage> {
847        None
848    }
849
850    /// Return stable identities aligned with this object's resident log records.
851    ///
852    /// Implementing log objects return identities oldest-to-newest, in the
853    /// same order as their public resident-record view and `LOG_BUFFER`
854    /// projection. Sequence numbers are object-owned metadata and never part
855    /// of a projected BACnet record payload. Non-log objects return `None`.
856    fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
857        None
858    }
859
860    /// Add a trend log record (only meaningful for TrendLog / TrendLogMultiple).
861    ///
862    /// Default is a no-op. TrendLog objects override to append to their buffer.
863    fn add_trend_record(&mut self, _record: BACnetLogRecord) {}
864
865    /// Fallible internal trend-record insertion used by the server poller.
866    ///
867    /// The default preserves source compatibility with existing implementors by
868    /// invoking the legacy void hook and reporting success. Built-in log objects
869    /// override this to surface mandatory status-timestamp failures atomically.
870    #[doc(hidden)]
871    fn try_add_trend_record_internal(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
872        self.add_trend_record(record);
873        Ok(())
874    }
875}