Source code for bac_py.services.common

"""Shared BACnet service data types per ASHRAE 135-2016 Clause 21."""

from __future__ import annotations

from dataclasses import dataclass

from bac_py.encoding.primitives import (
    decode_unsigned,
    encode_context_tagged,
    encode_unsigned,
)
from bac_py.encoding.tags import (
    TagClass,
    as_memoryview,
    decode_tag,
    encode_closing_tag,
    encode_opening_tag,
    extract_context_value,
)
from bac_py.services.errors import BACnetRejectError
from bac_py.types.enums import PropertyIdentifier, RejectReason


[docs] @dataclass(frozen=True, slots=True) class BACnetPropertyValue: """BACnetPropertyValue per Clause 21. :: BACnetPropertyValue ::= SEQUENCE { propertyIdentifier [0] BACnetPropertyIdentifier, propertyArrayIndex [1] Unsigned OPTIONAL, value [2] ABSTRACT-SYNTAX.&Type, priority [3] Unsigned (1..16) OPTIONAL } The ``value`` field contains raw application-tagged bytes. """ property_identifier: PropertyIdentifier property_array_index: int | None = None value: bytes = b"" priority: int | None = None
[docs] def encode(self) -> bytes: """Encode BACnetPropertyValue. :returns: Encoded bytes for this property value sequence. """ buf = bytearray() # [0] propertyIdentifier buf.extend(encode_context_tagged(0, encode_unsigned(self.property_identifier))) # [1] propertyArrayIndex (optional) if self.property_array_index is not None: buf.extend(encode_context_tagged(1, encode_unsigned(self.property_array_index))) # [2] value (opening/closing tag with raw application-tagged content) buf.extend(encode_opening_tag(2)) buf.extend(self.value) buf.extend(encode_closing_tag(2)) # [3] priority (optional) if self.priority is not None: buf.extend(encode_context_tagged(3, encode_unsigned(self.priority))) return bytes(buf)
[docs] @classmethod def decode_from( cls, data: memoryview | bytes, offset: int = 0 ) -> tuple[BACnetPropertyValue, int]: """Decode BACnetPropertyValue from data at a given offset. :param data: Raw bytes containing encoded property value data. :param offset: Byte offset to start decoding from. :returns: Tuple of (decoded :class:`BACnetPropertyValue`, new offset). :raises BACnetRejectError: If *priority* is outside the 1--16 range. """ data = as_memoryview(data) # [0] propertyIdentifier tag, offset = decode_tag(data, offset) property_identifier = PropertyIdentifier( decode_unsigned(data[offset : offset + tag.length]) ) offset += tag.length # [1] propertyArrayIndex (optional) property_array_index = None tag, new_offset = decode_tag(data, offset) if tag.cls == TagClass.CONTEXT and tag.number == 1 and not tag.is_opening: property_array_index = decode_unsigned(data[new_offset : new_offset + tag.length]) offset = new_offset + tag.length tag, new_offset = decode_tag(data, offset) # [2] value — opening tag 2 value, offset = extract_context_value(data, new_offset, 2) # [3] priority (optional, 1-16 per Clause 21) priority = None if offset < len(data): tag, new_offset = decode_tag(data, offset) if ( tag.cls == TagClass.CONTEXT and tag.number == 3 and not tag.is_opening and not tag.is_closing ): priority = decode_unsigned(data[new_offset : new_offset + tag.length]) offset = new_offset + tag.length if not (1 <= priority <= 16): raise BACnetRejectError(RejectReason.PARAMETER_OUT_OF_RANGE) return ( cls( property_identifier=property_identifier, property_array_index=property_array_index, value=value, priority=priority, ), offset, )