Source code for bac_py.types.parsing

"""Flexible parsing helpers for BACnet object and property identifiers."""

from __future__ import annotations

from functools import lru_cache

from bac_py.types.enums import ObjectType, PropertyIdentifier
from bac_py.types.primitives import ObjectIdentifier

OBJECT_TYPE_ALIASES: dict[str, ObjectType] = {
    # Analog I/O
    "ai": ObjectType.ANALOG_INPUT,
    "ao": ObjectType.ANALOG_OUTPUT,
    "av": ObjectType.ANALOG_VALUE,
    "lav": ObjectType.LARGE_ANALOG_VALUE,
    # Binary I/O
    "bi": ObjectType.BINARY_INPUT,
    "bo": ObjectType.BINARY_OUTPUT,
    "bv": ObjectType.BINARY_VALUE,
    "blo": ObjectType.BINARY_LIGHTING_OUTPUT,
    # Multi-state I/O
    "msi": ObjectType.MULTI_STATE_INPUT,
    "mso": ObjectType.MULTI_STATE_OUTPUT,
    "msv": ObjectType.MULTI_STATE_VALUE,
    # Infrastructure
    "dev": ObjectType.DEVICE,
    "file": ObjectType.FILE,
    "nc": ObjectType.NOTIFICATION_CLASS,
    "np": ObjectType.NETWORK_PORT,
    "cal": ObjectType.CALENDAR,
    "cmd": ObjectType.COMMAND,
    "ch": ObjectType.CHANNEL,
    "prog": ObjectType.PROGRAM,
    # Scheduling & trending
    "sched": ObjectType.SCHEDULE,
    "tl": ObjectType.TREND_LOG,
    "tlm": ObjectType.TREND_LOG_MULTIPLE,
    "el": ObjectType.EVENT_LOG,
    # Control
    "lp": ObjectType.LOOP,
    "lo": ObjectType.LIGHTING_OUTPUT,
    "lc": ObjectType.LOAD_CONTROL,
    "acc": ObjectType.ACCUMULATOR,
    "pc": ObjectType.PULSE_CONVERTER,
    "tmr": ObjectType.TIMER,
    # Monitoring & events
    "ee": ObjectType.EVENT_ENROLLMENT,
    "ae": ObjectType.ALERT_ENROLLMENT,
    "nf": ObjectType.NOTIFICATION_FORWARDER,
    "avg": ObjectType.AVERAGING,
    # Value types
    "iv": ObjectType.INTEGER_VALUE,
    "piv": ObjectType.POSITIVE_INTEGER_VALUE,
    "csv": ObjectType.CHARACTERSTRING_VALUE,
    "bsv": ObjectType.BITSTRING_VALUE,
    "osv": ObjectType.OCTETSTRING_VALUE,
    "dv": ObjectType.DATE_VALUE,
    "dtv": ObjectType.DATETIME_VALUE,
    "tv": ObjectType.TIME_VALUE,
    # Structure & grouping
    "sv": ObjectType.STRUCTURED_VIEW,
    "grp": ObjectType.GROUP,
    "gg": ObjectType.GLOBAL_GROUP,
    # Specialty
    "lsp": ObjectType.LIFE_SAFETY_POINT,
    "lsz": ObjectType.LIFE_SAFETY_ZONE,
    # Access control
    "ad": ObjectType.ACCESS_DOOR,
    "ap": ObjectType.ACCESS_POINT,
    # Audit
    "ar": ObjectType.AUDIT_REPORTER,
    "al": ObjectType.AUDIT_LOG,
}
"""Short aliases for commonly used object types.

Full hyphenated names (e.g. ``"analog-input"``) and underscore names
(e.g. ``"ANALOG_INPUT"``) are always accepted via :func:`_resolve_object_type`
without needing an alias entry here.
"""

PROPERTY_ALIASES: dict[str, PropertyIdentifier] = {
    # Core properties
    "pv": PropertyIdentifier.PRESENT_VALUE,
    "name": PropertyIdentifier.OBJECT_NAME,
    "type": PropertyIdentifier.OBJECT_TYPE,
    "desc": PropertyIdentifier.DESCRIPTION,
    "units": PropertyIdentifier.UNITS,
    "status": PropertyIdentifier.STATUS_FLAGS,
    "oos": PropertyIdentifier.OUT_OF_SERVICE,
    "reliability": PropertyIdentifier.RELIABILITY,
    "event-state": PropertyIdentifier.EVENT_STATE,
    # Object list & identification
    "list": PropertyIdentifier.OBJECT_LIST,
    "prop-list": PropertyIdentifier.PROPERTY_LIST,
    "profile-name": PropertyIdentifier.PROFILE_NAME,
    # Commandable properties
    "priority": PropertyIdentifier.PRIORITY_ARRAY,
    "relinquish": PropertyIdentifier.RELINQUISH_DEFAULT,
    # Analog properties
    "min": PropertyIdentifier.MIN_PRES_VALUE,
    "max": PropertyIdentifier.MAX_PRES_VALUE,
    "res": PropertyIdentifier.RESOLUTION,
    "cov-inc": PropertyIdentifier.COV_INCREMENT,
    "deadband": PropertyIdentifier.DEADBAND,
    "high-limit": PropertyIdentifier.HIGH_LIMIT,
    "low-limit": PropertyIdentifier.LOW_LIMIT,
    # Binary/multistate properties
    "polarity": PropertyIdentifier.POLARITY,
    "active-text": PropertyIdentifier.ACTIVE_TEXT,
    "inactive-text": PropertyIdentifier.INACTIVE_TEXT,
    "num-states": PropertyIdentifier.NUMBER_OF_STATES,
    "state-text": PropertyIdentifier.STATE_TEXT,
    # Event/alarm properties
    "event-enable": PropertyIdentifier.EVENT_ENABLE,
    "acked-transitions": PropertyIdentifier.ACKED_TRANSITIONS,
    "notify-type": PropertyIdentifier.NOTIFY_TYPE,
    "time-delay": PropertyIdentifier.TIME_DELAY,
    "notify-class": PropertyIdentifier.NOTIFICATION_CLASS,
    "limit-enable": PropertyIdentifier.LIMIT_ENABLE,
    # Scheduling & trending
    "log-buffer": PropertyIdentifier.LOG_BUFFER,
    "record-count": PropertyIdentifier.RECORD_COUNT,
    "enable": PropertyIdentifier.LOG_ENABLE,
    "weekly-schedule": PropertyIdentifier.WEEKLY_SCHEDULE,
    "exception-schedule": PropertyIdentifier.EXCEPTION_SCHEDULE,
    "schedule-default": PropertyIdentifier.SCHEDULE_DEFAULT,
    # Device properties
    "system-status": PropertyIdentifier.SYSTEM_STATUS,
    "vendor-name": PropertyIdentifier.VENDOR_NAME,
    "vendor-id": PropertyIdentifier.VENDOR_IDENTIFIER,
    "model-name": PropertyIdentifier.MODEL_NAME,
    "firmware-rev": PropertyIdentifier.FIRMWARE_REVISION,
    "app-version": PropertyIdentifier.APPLICATION_SOFTWARE_VERSION,
    "protocol-version": PropertyIdentifier.PROTOCOL_VERSION,
    "protocol-revision": PropertyIdentifier.PROTOCOL_REVISION,
    "max-apdu": PropertyIdentifier.MAX_APDU_LENGTH_ACCEPTED,
    "seg-supported": PropertyIdentifier.SEGMENTATION_SUPPORTED,
    "db-revision": PropertyIdentifier.DATABASE_REVISION,
}
"""Short aliases for commonly used property identifiers.

Full hyphenated names (e.g. ``"present-value"``) and underscore names
(e.g. ``"PRESENT_VALUE"``) are always accepted via
:func:`_resolve_property_identifier` without needing an alias entry here.
"""


@lru_cache(maxsize=256)
def _resolve_object_type(name: str) -> ObjectType:
    """Resolve an object type name to an :class:`~bac_py.types.enums.ObjectType`.

    Accepts short aliases (e.g. ``"ai"``), hyphenated names
    (e.g. ``"analog-input"``), underscore names (e.g. ``"ANALOG_INPUT"``),
    or raw integer strings.

    Results are cached so repeated lookups of the same alias (e.g. ``"ai"``)
    are O(1) after the first call.

    :param name: Object type name in any supported format.
    :returns: Resolved :class:`~bac_py.types.enums.ObjectType` member.
    :raises ValueError: If *name* is not recognised.
    """
    lower = name.lower().strip()

    if lower in OBJECT_TYPE_ALIASES:
        return OBJECT_TYPE_ALIASES[lower]

    enum_name = lower.replace("-", "_").upper()
    try:
        return ObjectType[enum_name]
    except KeyError:
        pass

    try:
        return ObjectType(int(lower))
    except (ValueError, KeyError):
        pass

    msg = f"Unknown object type: {name!r}"
    raise ValueError(msg)


[docs] def parse_object_identifier( obj: str | tuple[str | ObjectType | int, int] | ObjectIdentifier, ) -> ObjectIdentifier: """Parse a flexible object identifier to :class:`~bac_py.types.primitives.ObjectIdentifier`. Accepted formats:: "analog-input,1" -> ObjectIdentifier(ANALOG_INPUT, 1) "analog-input:1" -> ObjectIdentifier(ANALOG_INPUT, 1) "ai,1" -> ObjectIdentifier(ANALOG_INPUT, 1) ("analog-input", 1) -> ObjectIdentifier(ANALOG_INPUT, 1) (ObjectType.ANALOG_INPUT, 1) -> ObjectIdentifier(ANALOG_INPUT, 1) (0, 1) -> ObjectIdentifier(ANALOG_INPUT, 1) ObjectIdentifier(...) -> pass-through :param obj: Object identifier in any supported format. :returns: Parsed :class:`~bac_py.types.primitives.ObjectIdentifier`. :raises ValueError: If the format is not recognised. """ if isinstance(obj, ObjectIdentifier): return obj if isinstance(obj, tuple): if len(obj) != 2: msg = f"Object identifier tuple must have 2 elements, got {len(obj)}" raise ValueError(msg) type_part, instance = obj if isinstance(type_part, ObjectType): return ObjectIdentifier(type_part, instance) if isinstance(type_part, int): return ObjectIdentifier(ObjectType(type_part), instance) if isinstance(type_part, str): return ObjectIdentifier(_resolve_object_type(type_part), instance) msg = f"Cannot parse object type from {type(type_part).__name__}" raise ValueError(msg) if isinstance(obj, str): for sep in (",", ":"): if sep in obj: parts = obj.split(sep, 1) type_name = parts[0].strip() try: instance = int(parts[1].strip()) except ValueError: msg = f"Invalid instance number in {obj!r}" raise ValueError(msg) from None return ObjectIdentifier(_resolve_object_type(type_name), instance) msg = ( f"Cannot parse object identifier: {obj!r}. " "Expected format like 'analog-input,1' or 'ai:1'" ) raise ValueError(msg) msg = f"Cannot parse object identifier from {type(obj).__name__}" raise ValueError(msg)
@lru_cache(maxsize=512) def _resolve_property_identifier(name: str) -> PropertyIdentifier: """Resolve a property name to a :class:`~bac_py.types.enums.PropertyIdentifier`. Accepts short aliases (e.g. ``"pv"``), hyphenated names (e.g. ``"present-value"``), underscore names (e.g. ``"PRESENT_VALUE"``), or raw integer strings. Results are cached so repeated lookups of the same alias (e.g. ``"pv"``) are O(1) after the first call. :param name: Property name in any supported format. :returns: Resolved :class:`~bac_py.types.enums.PropertyIdentifier` member. :raises ValueError: If *name* is not recognised. """ lower = name.lower().strip() if lower in PROPERTY_ALIASES: return PROPERTY_ALIASES[lower] enum_name = lower.replace("-", "_").upper() try: return PropertyIdentifier[enum_name] except KeyError: pass try: return PropertyIdentifier(int(lower)) except (ValueError, KeyError): pass msg = f"Unknown property identifier: {name!r}" raise ValueError(msg)
[docs] def parse_property_identifier( prop: str | int | PropertyIdentifier, ) -> PropertyIdentifier: """Parse a flexible property identifier to :class:`~bac_py.types.enums.PropertyIdentifier`. Accepted formats:: "present-value" -> PropertyIdentifier.PRESENT_VALUE "present_value" -> PropertyIdentifier.PRESENT_VALUE "pv" -> PropertyIdentifier.PRESENT_VALUE "object-name" -> PropertyIdentifier.OBJECT_NAME "name" -> PropertyIdentifier.OBJECT_NAME 85 -> PropertyIdentifier(85) PropertyIdentifier.PRESENT_VALUE -> pass-through :param prop: Property identifier in any supported format. :returns: Parsed :class:`~bac_py.types.enums.PropertyIdentifier`. :raises ValueError: If the format is not recognised. """ if isinstance(prop, PropertyIdentifier): return prop if isinstance(prop, int): return PropertyIdentifier(prop) if isinstance(prop, str): return _resolve_property_identifier(prop) msg = f"Cannot parse property identifier from {type(prop).__name__}" raise ValueError(msg)