"""
Module for serialization and deserialization of KNX DIB information.
DIB is Description Information Block.
A KNX/IP Search Response may contain several DIBs of different types:
* DIBSuppSVCFamilies: Supported features of device
* DIBDeviceInformation: Name, serial number, some unimportant flags
* DIBGeneric: General Information
(fallback for unknown dib type codes)
"""
from __future__ import annotations
from abc import ABC, abstractmethod
import socket
from typing import NamedTuple, final
from xknx.exceptions import CouldNotParseKNXIP
from xknx.telegram import IndividualAddress
from .knxip_enum import DIBServiceFamily, DIBTypeCode, KNXMedium
DIB_HEADER_LENGTH = 2 # structure length and description type code
class DIB(ABC):
"""
Base class for DIB (Description Information Block).
This base class is only the interface for the derived
classes.
"""
@abstractmethod
def calculated_length(self) -> int:
"""Get length of KNX/IP object."""
# The structure shall always have an even number of octets which may have to be
# achieved by padding with 00h in the last octet of the DIB structure.
@abstractmethod
def from_knx(self, raw: bytes) -> int:
"""Parse/deserialize from KNX/IP raw data."""
@abstractmethod
def to_knx(self) -> bytes:
"""Serialize to KNX/IP raw data."""
@staticmethod
def determine_dib(raw: bytes) -> DIB:
"""Determine dib type out of dib type code."""
if len(raw) < 2:
raise CouldNotParseKNXIP("could not parse DIB header")
dtc = DIBTypeCode(raw[1])
if dtc == DIBTypeCode.DEVICE_INFO:
return DIBDeviceInformation()
if dtc == DIBTypeCode.SUPP_SVC_FAMILIES:
return DIBSuppSVCFamilies()
if dtc == DIBTypeCode.SECURED_SERVICE_FAMILIES:
return DIBSecuredServiceFamilies()
if dtc == DIBTypeCode.TUNNELING_INFO:
return DIBTunnelingInfo()
return DIBGeneric()
class DIBGeneric(DIB):
"""
Module for serialization and deserialization of KNX DIB Generic.
Fallback for not implemented DIBTypeCodes.
"""
def __init__(self) -> None:
"""Initialize DIBGeneric class."""
# DTC Description Type Code
self.dtc: DIBTypeCode | int = 0
# IBD Information Block Data
self.data = b""
def calculated_length(self) -> int:
"""Get length of KNX/IP object."""
data_length = len(self.data)
return DIB_HEADER_LENGTH + data_length + data_length % 2
def from_knx(self, raw: bytes) -> int:
"""Parse/deserialize from KNX/IP raw data."""
if len(raw) < 2:
raise CouldNotParseKNXIP("could not parse DIB header")
dib_length = raw[0]
if len(raw) < dib_length:
raise CouldNotParseKNXIP("DIB wrong length")
try:
self.dtc = DIBTypeCode(raw[1])
except ValueError:
self.dtc = raw[1]
self.data = raw[2:dib_length]
return dib_length
def to_knx(self) -> bytes:
"""Serialize to KNX/IP raw data."""
if not isinstance(self.dtc, DIBTypeCode):
try:
self.dtc = DIBTypeCode(self.dtc)
except ValueError:
raise CouldNotParseKNXIP("DTC invalid") from None
return (
bytes((self.calculated_length(), self.dtc.value))
+ self.data
+ bytes(len(self.data) % 2) # padding
)
def __repr__(self) -> str:
"""Return object as readable string."""
return f''
@final
class DIBDeviceInformation(DIB):
"""Class for serialization and deserialization of KNX DIB Device Information Block."""
LENGTH = 54
def __init__(self) -> None:
"""Initialize DIBDeviceInformation class."""
self.knx_medium: KNXMedium = KNXMedium.TP1
self.programming_mode: bool = False
self.individual_address: IndividualAddress = IndividualAddress(0)
self.installation_number: int = 0
self.project_number: int = 0
self.serial_number: str = ""
self.multicast_address: str = "224.0.23.12"
self.mac_address: str = ""
self.name: str = ""
def calculated_length(self) -> int:
"""Get length of KNX/IP object."""
return DIBDeviceInformation.LENGTH
def from_knx(self, raw: bytes) -> int:
"""Parse/deserialize from KNX/IP raw data."""
if len(raw) < DIBDeviceInformation.LENGTH:
raise CouldNotParseKNXIP("wrong connection header length")
if raw[0] != DIBDeviceInformation.LENGTH:
raise CouldNotParseKNXIP("wrong connection header length")
if DIBTypeCode(raw[1]) != DIBTypeCode.DEVICE_INFO:
raise CouldNotParseKNXIP("DIB is no device info")
self.knx_medium = KNXMedium(raw[2])
# last bit of device_status. All other bits are unused
self.programming_mode = bool(raw[3])
self.individual_address = IndividualAddress.from_knx(raw[4:6])
installation_project_identifier = raw[6] * 256 + raw[7]
self.project_number = installation_project_identifier >> 4
self.installation_number = installation_project_identifier & 15
self.serial_number = raw[8:14].hex(":")
self.multicast_address = socket.inet_ntoa(raw[14:18])
self.mac_address = raw[18:24].hex(":")
self.name = raw[24:54].decode(encoding="latin_1", errors="replace").rstrip("\0")
return DIBDeviceInformation.LENGTH
def to_knx(self) -> bytes:
"""Serialize to KNX/IP raw data."""
def hex_notation_to_knx(colon_hex: str) -> bytes:
"""Serialize hex notation."""
return bytes.fromhex(colon_hex.replace(":", ""))
def ip_to_knx(ip_addr: str) -> bytes:
"""Serialize ip."""
return socket.inet_aton(ip_addr)
def name_str_to_knx(string: str) -> bytes:
"""Serialize name string."""
# pad with null bytes to length 30; ISO 8859-1 (latin_1) according to KNX specification
return bytes(string[:30], "latin_1").ljust(30, b"\0")
installation_project_identifier = (
(self.project_number * 16) + self.installation_number
).to_bytes(2, "big")
return (
bytes(
(
DIBDeviceInformation.LENGTH,
DIBTypeCode.DEVICE_INFO.value,
self.knx_medium.value,
self.programming_mode,
)
)
+ self.individual_address.to_knx()
+ installation_project_identifier
+ hex_notation_to_knx(self.serial_number)
+ ip_to_knx(self.multicast_address)
+ hex_notation_to_knx(self.mac_address)
+ name_str_to_knx(self.name)
)
def __repr__(self) -> str:
"""Return object as readable string."""
return (
"'
)
class _DIBServiceFamilies(DIB):
"""Base class for serialization and deserialization of KNX DIB Service Families."""
type_code: DIBTypeCode
class Family:
"""Class for storing a supported device family."""
def __init__(self, name: DIBServiceFamily, version: int) -> None:
"""Initialize DIBSuppSVCFamilies.Family."""
self.name = name
self.version = version
def to_knx(self) -> bytes:
"""Serialize to KNX/IP raw data."""
return bytes((self.name.value, self.version))
def __repr__(self) -> str:
"""Return object as readable string."""
return f''
def __eq__(self, other: object) -> bool:
"""Equal operator."""
return self.__dict__ == other.__dict__
def __init__(self) -> None:
"""Initialize DIBSuppSVCFamilies class."""
self.families: list[DIBSuppSVCFamilies.Family] = []
def supports(self, name: DIBServiceFamily, version: int | None = None) -> bool:
"""Return if device supports a given service family by name and optional minimum version."""
return any(
name == family.name and (version is None or family.version >= version)
for family in self.families
)
def version(self, name: DIBServiceFamily) -> int | None:
"""Return version of a given service family."""
return next(
(family.version for family in self.families if name == family.name),
None,
)
def calculated_length(self) -> int:
"""Get length of KNX/IP object."""
return len(self.families) * 2 + DIB_HEADER_LENGTH
def from_knx(self, raw: bytes) -> int:
"""Parse/deserialize from KNX/IP raw data."""
if len(raw) < 2:
raise CouldNotParseKNXIP("DIB header too small")
length = raw[0]
if (len(raw) < length) or (length % 2):
raise CouldNotParseKNXIP("DIB wrong size")
if DIBTypeCode(raw[1]) != self.type_code:
raise CouldNotParseKNXIP(
f"DIB has wrong type code for {self.__class__.__name__}"
)
for pos in range(2, length, 2):
name = DIBServiceFamily(raw[pos])
version = raw[pos + 1]
self.families.append(DIBSuppSVCFamilies.Family(name, version))
return length
def to_knx(self) -> bytes:
"""Serialize to KNX/IP raw data."""
return bytes(
(
self.calculated_length(),
self.type_code.value,
)
) + b"".join(family.to_knx() for family in self.families)
def __repr__(self) -> str:
"""Return object as readable string."""
_families_str = ", ".join(
f"{family.name} version: {family.version}" for family in self.families
)
return f'<{self.__class__.__name__} families="[{_families_str}]" />'
@final
class DIBSuppSVCFamilies(_DIBServiceFamilies):
"""Class for serialization and deserialization of KNX DIB Supported Services."""
type_code = DIBTypeCode.SUPP_SVC_FAMILIES
@final
class DIBSecuredServiceFamilies(_DIBServiceFamilies):
"""Class for serialization and deserialization of KNX DIB Secured Service Families."""
type_code = DIBTypeCode.SECURED_SERVICE_FAMILIES
class TunnelingSlotStatus(NamedTuple):
"""Class for storing tunneling slot status."""
usable: bool
authorized: bool
free: bool
def __bytes__(self) -> bytes:
"""Serialize to KNX/IP raw data."""
return bytes(
(
0x00, # reserved
self.usable << 2 | self.authorized << 1 | self.free,
)
)
@final
class DIBTunnelingInfo(DIB):
"""Class for serialization and deserialization of KNX DIB Tunneling Info."""
def __init__(
self, slots: dict[IndividualAddress, TunnelingSlotStatus] | None = None
) -> None:
"""Initialize DIBTunnelingInfo class."""
self.max_apdu_length = 248
self.slots = slots or {}
def calculated_length(self) -> int:
"""Get length of KNX/IP object."""
return 2 + 2 + len(self.slots) * 4
def from_knx(self, raw: bytes) -> int:
"""Parse/deserialize from KNX/IP raw data."""
if len(raw) < 4:
raise CouldNotParseKNXIP("DIB header too small")
length = raw[0]
if (len(raw) < length) or (length % 4):
raise CouldNotParseKNXIP("DIB wrong size")
if DIBTypeCode(raw[1]) != DIBTypeCode.TUNNELING_INFO:
raise CouldNotParseKNXIP(
f"DIB has wrong type code for {self.__class__.__name__}"
)
self.max_apdu_length = int.from_bytes(raw[2:4], "big")
for pos in range(4, length, 4):
address = IndividualAddress.from_knx(raw[pos : pos + 2])
status = TunnelingSlotStatus(
usable=bool(raw[pos + 3] >> 2 & 0b1),
authorized=bool(raw[pos + 3] >> 1 & 0b1),
free=bool(raw[pos + 3] & 0b1),
)
self.slots[address] = status
return length
def to_knx(self) -> bytes:
"""Serialize to KNX/IP raw data."""
return (
bytes((self.calculated_length(), DIBTypeCode.TUNNELING_INFO.value))
+ self.max_apdu_length.to_bytes(2, "big")
+ b"".join(
address.to_knx() + bytes(status)
for address, status in self.slots.items()
)
)
def __repr__(self) -> str:
"""Return object as readable string."""
return (
f"<{self.__class__.__name__} max_adpu_lenght={self.max_apdu_length} "
f"slots={self.slots}/>"
)