Source code for bac_py.services.fault_algorithms

"""Fault algorithm evaluators per ASHRAE 135-2020 Clause 13.4.

Each public function evaluates one fault algorithm variant.  They are
pure functions -- no I/O, no async, no side effects -- making them
straightforward to unit-test.

**Contract with the caller:**  The ``reliability_evaluation_inhibit``
property (PropertyIdentifier 357) is checked by the caller *before*
invoking any evaluator.  When that flag is ``True`` the caller must
skip evaluation and keep the current ``Reliability`` value unchanged.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from bac_py.types.enums import Reliability

if TYPE_CHECKING:
    from collections.abc import Callable
    from typing import Any

    from bac_py.types.constructed import StatusFlags
    from bac_py.types.enums import LifeSafetyState
    from bac_py.types.fault_params import (
        FaultCharacterString,
        FaultExtended,
        FaultLifeSafety,
        FaultListed,
        FaultOutOfRange,
        FaultState,
        FaultStatusFlags,
    )


[docs] def evaluate_fault_characterstring( current_value: str, params: FaultCharacterString, ) -> Reliability: """Evaluate FAULT_CHARACTERSTRING (Clause 13.4.1). If *current_value* matches any string in ``params.fault_values``, return ``MULTI_STATE_FAULT``; otherwise return ``NO_FAULT_DETECTED``. :param current_value: The current character-string property value. :param params: Fault parameter configuration carrying the list of fault-triggering strings. :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ if current_value in params.fault_values: return Reliability.MULTI_STATE_FAULT return Reliability.NO_FAULT_DETECTED
[docs] def evaluate_fault_extended( current_value: Any, params: FaultExtended, *, vendor_callback: Callable[[Any, FaultExtended], Reliability] | None = None, ) -> Reliability: """Evaluate FAULT_EXTENDED (Clause 13.4.2). Vendor-specific algorithm. If *vendor_callback* is provided the evaluation is delegated to it; otherwise ``NO_FAULT_DETECTED`` is returned because the algorithm cannot be evaluated without vendor-supplied logic. :param current_value: The current property value (type varies by vendor). :param params: Fault parameter configuration with vendor-id, extended-fault-type, and raw parameter bytes. :param vendor_callback: Optional callable implementing the vendor-specific evaluation logic. :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ if vendor_callback is not None: return vendor_callback(current_value, params) return Reliability.NO_FAULT_DETECTED
[docs] def evaluate_fault_life_safety( current_value: LifeSafetyState, params: FaultLifeSafety, ) -> Reliability: """Evaluate FAULT_LIFE_SAFETY (Clause 13.4.3). If *current_value* appears in ``params.fault_values``, return ``MULTI_STATE_FAULT``. Mode filtering (``params.mode_values``) is the responsibility of the caller -- it determines *whether* this evaluator should be invoked at all. :param current_value: The current life-safety state. :param params: Fault parameter configuration carrying fault values and mode values. :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ if current_value in params.fault_values: return Reliability.MULTI_STATE_FAULT return Reliability.NO_FAULT_DETECTED
[docs] def evaluate_fault_state( current_value: int, params: FaultState, *, fault_enum_values: tuple[int, ...] = (), ) -> Reliability: """Evaluate FAULT_STATE (Clause 13.4.4). Because ``BACnetPropertyStates`` is a large CHOICE type whose ``params.fault_values`` are stored as raw bytes, the caller pre-parses them into *fault_enum_values* -- a tuple of integer enumeration values to match against. :param current_value: The current enumerated property value (as an integer). :param params: Fault parameter configuration (used for reference; the actual match set is *fault_enum_values*). :param fault_enum_values: Pre-parsed integer enumeration values derived from ``params.fault_values``. :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ # params is kept in the signature for API consistency and to allow # future use; currently the pre-parsed values are authoritative. _ = params if current_value in fault_enum_values: return Reliability.MULTI_STATE_FAULT return Reliability.NO_FAULT_DETECTED
[docs] def evaluate_fault_status_flags( current_flags: StatusFlags, params: FaultStatusFlags, ) -> Reliability: """Evaluate FAULT_STATUS_FLAGS (Clause 13.4.5). If the *fault* bit in *current_flags* is set, return ``MEMBER_FAULT``. The ``params.status_flags_ref`` tells the caller *where* to read the flags from; the actual flag values are supplied as *current_flags*. :param current_flags: The resolved status-flags value. :param params: Fault parameter configuration carrying the property reference (used by the caller, not by this evaluator). :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ _ = params if current_flags.fault: return Reliability.MEMBER_FAULT return Reliability.NO_FAULT_DETECTED
[docs] def evaluate_fault_out_of_range( current_value: float | int, params: FaultOutOfRange, ) -> Reliability: """Evaluate FAULT_OUT_OF_RANGE (Clause 13.4.6). Returns ``UNDER_RANGE`` when *current_value* is strictly less than ``params.min_normal_value``, ``OVER_RANGE`` when strictly greater than ``params.max_normal_value``, and ``NO_FAULT_DETECTED`` otherwise. Values exactly at the boundary are *not* faulted. :param current_value: The current analog property value. :param params: Fault parameter configuration with min/max normal value boundaries. :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ if current_value < params.min_normal_value: return Reliability.UNDER_RANGE if current_value > params.max_normal_value: return Reliability.OVER_RANGE return Reliability.NO_FAULT_DETECTED
[docs] def evaluate_fault_listed( current_value: Any, params: FaultListed, *, fault_list: tuple[tuple[Any, Callable[..., Reliability]], ...] = (), ) -> Reliability: """Evaluate FAULT_LISTED (Clause 13.4.7). Iterates over *fault_list* -- a sequence of ``(fault_params, evaluator_fn)`` pairs -- and returns the first non-``NO_FAULT_DETECTED`` result. If every evaluator returns ``NO_FAULT_DETECTED`` (or the list is empty), ``NO_FAULT_DETECTED`` is returned. ``params.fault_list_ref`` tells the caller *where* to read the fault list; the resolved list content is supplied as *fault_list*. :param current_value: The current property value forwarded to each sub-evaluator. :param params: Fault parameter configuration carrying the property reference (used by the caller, not directly by this evaluator). :param fault_list: Pre-resolved sequence of fault parameter / evaluator pairs. :returns: Computed :class:`~bac_py.types.enums.Reliability` value. """ _ = params for sub_params, evaluator_fn in fault_list: result = evaluator_fn(current_value, sub_params) if result != Reliability.NO_FAULT_DETECTED: return result return Reliability.NO_FAULT_DETECTED