Esempio n. 1
0
class DIBDeviceInformation(DIB):
    """Class for serialization and deserialization of KNX DIB Device Information Block."""

    # pylint: disable=too-many-instance-attributes

    LENGTH = 54

    def __init__(self) -> None:
        """Initialize DIBDeviceInformation class."""
        super().__init__()
        self.knx_medium: KNXMedium = KNXMedium.TP1
        self.programming_mode: bool = False
        self.individual_address: IndividualAddress = IndividualAddress(None)
        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((raw[4], raw[5]))
        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 = ":".join("%02x" % i for i in raw[8:14])
        self.multicast_address = ".".join("%i" % i for i in raw[14:18])
        self.mac_address = ":".join("%02x" % i for i in raw[18:24])
        self.name = "".join(map(chr, raw[24:54])).rstrip("\0")
        return DIBDeviceInformation.LENGTH

    def to_knx(self) -> List[int]:
        """Serialize to KNX/IP raw data."""

        def hex_notation_to_knx(serial_number: str) -> Iterator[int]:
            """Serialize hex notation."""
            for part in serial_number.split(":"):
                yield int(part, 16)

        def ip_to_knx(ip_addr: str) -> Iterator[int]:
            """Serialize ip."""
            for part in ip_addr.split("."):
                yield int(part)

        def str_to_knx(string: str, length: int) -> Iterator[int]:
            """Serialize string."""
            if len(string) > length - 1:
                string = string[: length - 1]
            for char in string:
                yield ord(char)
            for _ in range(0, 30 - len(string)):
                yield 0x00

        installation_project_identifier = (
            self.project_number * 16
        ) + self.installation_number
        data = []
        data.append(DIBDeviceInformation.LENGTH)
        data.append(DIBTypeCode.DEVICE_INFO.value)
        data.append(self.knx_medium.value)
        data.append(int(self.programming_mode))
        data.extend(self.individual_address.to_knx())
        data.append((installation_project_identifier >> 8) & 255)
        data.append(installation_project_identifier & 255)
        data.extend(hex_notation_to_knx(self.serial_number))
        data.extend(ip_to_knx(self.multicast_address))
        data.extend(hex_notation_to_knx(self.mac_address))
        data.extend(str_to_knx(self.name, 30))
        return data

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            "<DIBDeviceInformation "
            '\n\tknx_medium="{}" '
            '\n\tprogramming_mode="{}" '
            '\n\tindividual_address="{}" '
            '\n\tinstallation_number="{}" '
            '\n\tproject_number="{}" '
            '\n\tserial_number="{}" '
            '\n\tmulticast_address="{}" '
            '\n\tmac_address="{}" '
            '\n\tname="{}" />'.format(
                self.knx_medium,
                self.programming_mode,
                self.individual_address,
                self.installation_number,
                self.project_number,
                self.serial_number,
                self.multicast_address,
                self.mac_address,
                self.name,
            )
        )
Esempio n. 2
0
File: dib.py Progetto: XKNX/xknx
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(None)
        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((raw[4], raw[5]))
        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,
        )) + bytes(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 ("<DIBDeviceInformation "
                f'\n\tknx_medium="{self.knx_medium}" '
                f'\n\tprogramming_mode="{self.programming_mode}" '
                f'\n\tindividual_address="{self.individual_address}" '
                f'\n\tinstallation_number="{self.installation_number}" '
                f'\n\tproject_number="{self.project_number}" '
                f'\n\tserial_number="{self.serial_number}" '
                f'\n\tmulticast_address="{self.multicast_address}" '
                f'\n\tmac_address="{self.mac_address}" '
                f'\n\tname="{self.name}" />')
Esempio n. 3
0
class CEMIFrame:
    """Representation of a CEMI Frame."""

    def __init__(
        self,
        xknx: XKNX,
        code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
        flags: int = 0,
        src_addr: IndividualAddress = IndividualAddress(None),
        dst_addr: GroupAddress | IndividualAddress = GroupAddress(None),
        mpdu_len: int = 0,
        payload: APCI | None = None,
    ):
        """Initialize CEMIFrame object."""
        self.xknx = xknx
        self.code = code
        self.flags = flags
        self.src_addr = src_addr
        self.dst_addr = dst_addr
        self.mpdu_len = mpdu_len
        self.payload = payload

    @staticmethod
    def init_from_telegram(
        xknx: XKNX,
        telegram: Telegram,
        code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
        src_addr: IndividualAddress = IndividualAddress(None),
    ) -> CEMIFrame:
        """Return CEMIFrame from a Telegram."""
        cemi = CEMIFrame(xknx, code=code, src_addr=src_addr)
        # dst_addr, payload and cmd are set by telegram.setter - mpdu_len not needed for outgoing telegram
        cemi.telegram = telegram
        return cemi

    @property
    def telegram(self) -> Telegram:
        """Return telegram."""

        return Telegram(
            destination_address=self.dst_addr,
            payload=self.payload,
            source_address=self.src_addr,
        )

    @telegram.setter
    def telegram(self, telegram: Telegram) -> None:
        """Set telegram."""
        # TODO: Move to separate function, together with setting of
        # CEMIMessageCode
        self.flags = (
            CEMIFlags.FRAME_TYPE_STANDARD
            | CEMIFlags.DO_NOT_REPEAT
            | CEMIFlags.BROADCAST
            | CEMIFlags.PRIORITY_LOW
            | CEMIFlags.NO_ACK_REQUESTED
            | CEMIFlags.CONFIRM_NO_ERROR
            | CEMIFlags.HOP_COUNT_1ST
        )

        if isinstance(telegram.destination_address, GroupAddress):
            self.flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS
        elif isinstance(telegram.destination_address, IndividualAddress):
            self.flags |= CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS
        else:
            raise TypeError()

        self.dst_addr = telegram.destination_address
        self.payload = telegram.payload

    def set_hops(self, hops: int) -> None:
        """Set hops."""
        # Resetting hops
        self.flags &= 0xFFFF ^ 0x0070
        # Setting new hops
        self.flags |= hops << 4

    def calculated_length(self) -> int:
        """Get length of KNX/IP body."""
        if not isinstance(self.payload, APCI):
            raise TypeError()
        return 10 + self.payload.calculated_length()

    def from_knx(self, raw: bytes) -> int:
        """Parse/deserialize from KNX/IP raw data."""
        try:
            self.code = CEMIMessageCode(raw[0])
        except ValueError:
            raise UnsupportedCEMIMessage(
                "CEMIMessageCode not implemented: {} in CEMI: {}".format(
                    raw[0], raw.hex()
                )
            )

        if self.code not in (
            CEMIMessageCode.L_DATA_IND,
            CEMIMessageCode.L_DATA_REQ,
            CEMIMessageCode.L_DATA_CON,
        ):
            raise UnsupportedCEMIMessage(
                "Could not handle CEMIMessageCode: {} / {} in CEMI: {}".format(
                    self.code, raw[0], raw.hex()
                )
            )

        return self.from_knx_data_link_layer(raw)

    def from_knx_data_link_layer(self, cemi: bytes) -> int:
        """Parse L_DATA_IND, CEMIMessageCode.L_DATA_REQ, CEMIMessageCode.L_DATA_CON."""
        if len(cemi) < 11:
            # eg. ETS Line-Scan issues L_DATA_IND with length 10
            raise UnsupportedCEMIMessage(
                "CEMI too small. Length: {}; CEMI: {}".format(len(cemi), cemi.hex())
            )

        # AddIL (Additional Info Length), as specified within
        # KNX Chapter 3.6.3/4.1.4.3 "Additional information."
        # Additional information is not yet parsed.
        addil = cemi[1]
        # Control field 1 and Control field 2 - first 2 octets after Additional information
        self.flags = cemi[2 + addil] * 256 + cemi[3 + addil]

        self.src_addr = IndividualAddress((cemi[4 + addil], cemi[5 + addil]))

        if self.flags & CEMIFlags.DESTINATION_GROUP_ADDRESS:
            self.dst_addr = GroupAddress(
                (cemi[6 + addil], cemi[7 + addil]), levels=self.xknx.address_format
            )
        else:
            self.dst_addr = IndividualAddress((cemi[6 + addil], cemi[7 + addil]))

        self.mpdu_len = cemi[8 + addil]

        # TPCI (transport layer control information)   -> First 14 bit
        # APCI (application layer control information) -> Last  10 bit

        apdu = cemi[9 + addil :]
        if len(apdu) != (self.mpdu_len + 1):
            raise CouldNotParseKNXIP(
                "APDU LEN should be {} but is {} in CEMI: {}".format(
                    self.mpdu_len, len(apdu), cemi.hex()
                )
            )

        tpci_apci = (apdu[0] << 8) + apdu[1]

        try:
            self.payload = APCI.resolve_apci(tpci_apci & 0x03FF)
        except ConversionError:
            raise UnsupportedCEMIMessage(
                "APCI not supported: {:#012b}".format(tpci_apci & 0x03FF)
            )

        self.payload.from_knx(apdu)

        return 10 + addil + self.mpdu_len

    def to_knx(self) -> list[int]:
        """Serialize to KNX/IP raw data."""
        if not isinstance(self.payload, APCI):
            raise TypeError()
        if not isinstance(self.src_addr, (GroupAddress, IndividualAddress)):
            raise ConversionError("src_addr not set")
        if not isinstance(self.dst_addr, (GroupAddress, IndividualAddress)):
            raise ConversionError("dst_addr not set")

        data = []

        data.append(self.code.value)
        data.append(0x00)
        data.append((self.flags >> 8) & 255)
        data.append(self.flags & 255)
        data.extend(self.src_addr.to_knx())
        data.extend(self.dst_addr.to_knx())
        data.append(self.payload.calculated_length())
        data.extend(self.payload.to_knx())

        return data

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            '<CEMIFrame SourceAddress="{}" DestinationAddress="{}" '
            'Flags="{:16b}" payload="{}" />'.format(
                self.src_addr.__repr__(),
                self.dst_addr.__repr__(),
                self.flags,
                self.payload,
            )
        )

    def __eq__(self, other: object) -> bool:
        """Equal operator."""
        return self.__dict__ == other.__dict__
Esempio n. 4
0
class CEMIFrame:
    """Representation of a CEMI Frame."""
    def __init__(
            self,
            code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
            flags: int = 0,
            src_addr: IndividualAddress = IndividualAddress(None),
            dst_addr: GroupAddress | IndividualAddress = GroupAddress(None),
            tpci: TPCI = TDataGroup(),
            payload: APCI | None = None,
    ):
        """Initialize CEMIFrame object."""
        self.code = code
        self.flags = flags
        self.src_addr = src_addr
        self.dst_addr = dst_addr
        self.tpci = tpci
        self.payload = payload

    @staticmethod
    def init_from_telegram(
            telegram: Telegram,
            code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
            src_addr: IndividualAddress = IndividualAddress(None),
    ) -> CEMIFrame:
        """Return CEMIFrame from a Telegram."""
        cemi = CEMIFrame(code=code, src_addr=src_addr)
        # dst_addr, payload and cmd are set by telegram.setter
        cemi.telegram = telegram
        return cemi

    @property
    def telegram(self) -> Telegram:
        """Return telegram."""

        return Telegram(
            destination_address=self.dst_addr,
            payload=self.payload,
            source_address=self.src_addr,
            tpci=self.tpci,
        )

    @telegram.setter
    def telegram(self, telegram: Telegram) -> None:
        """Set telegram."""
        # TODO: Move to separate function, together with setting of
        # CEMIMessageCode
        self.flags = (CEMIFlags.FRAME_TYPE_STANDARD
                      | CEMIFlags.DO_NOT_REPEAT
                      | CEMIFlags.BROADCAST
                      | CEMIFlags.NO_ACK_REQUESTED
                      | CEMIFlags.CONFIRM_NO_ERROR
                      | CEMIFlags.HOP_COUNT_1ST)

        if isinstance(telegram.destination_address, GroupAddress):
            self.flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS | CEMIFlags.PRIORITY_LOW
        elif isinstance(telegram.destination_address, IndividualAddress):
            self.flags |= (CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS
                           | CEMIFlags.PRIORITY_SYSTEM)
        else:
            raise TypeError()

        self.dst_addr = telegram.destination_address
        self.tpci = telegram.tpci
        self.payload = telegram.payload

    def set_hops(self, hops: int) -> None:
        """Set hops."""
        # Resetting hops
        self.flags &= 0xFFFF ^ 0x0070
        # Setting new hops
        self.flags |= hops << 4

    def calculated_length(self) -> int:
        """Get length of KNX/IP body."""
        if not self.tpci.control and self.payload is not None:
            return 10 + self.payload.calculated_length()
        if self.tpci.control and self.payload is None:
            return 10
        raise TypeError(
            "Data TPDU must have a payload; control TPDU must not.")

    def from_knx(self, raw: bytes) -> int:
        """Parse/deserialize from KNX/IP raw data."""
        try:
            self.code = CEMIMessageCode(raw[0])
        except ValueError:
            raise UnsupportedCEMIMessage(
                f"CEMIMessageCode not implemented: {raw[0]} in CEMI: {raw.hex()}"
            )

        if self.code not in (
                CEMIMessageCode.L_DATA_IND,
                CEMIMessageCode.L_DATA_REQ,
                CEMIMessageCode.L_DATA_CON,
        ):
            raise UnsupportedCEMIMessage(
                f"Could not handle CEMIMessageCode: {self.code} / {raw[0]} in CEMI: {raw.hex()}"
            )

        return self.from_knx_data_link_layer(raw)

    def from_knx_data_link_layer(self, cemi: bytes) -> int:
        """Parse L_DATA_IND, CEMIMessageCode.L_DATA_REQ, CEMIMessageCode.L_DATA_CON."""
        if len(cemi) < 10:
            raise UnsupportedCEMIMessage(
                f"CEMI too small. Length: {len(cemi)}; CEMI: {cemi.hex()}")

        # AddIL (Additional Info Length), as specified within
        # KNX Chapter 3.6.3/4.1.4.3 "Additional information."
        # Additional information is not yet parsed.
        addil = cemi[1]
        # Control field 1 and Control field 2 - first 2 octets after Additional information
        self.flags = cemi[2 + addil] * 256 + cemi[3 + addil]

        self.src_addr = IndividualAddress((cemi[4 + addil], cemi[5 + addil]))

        dst_is_group_address = bool(self.flags
                                    & CEMIFlags.DESTINATION_GROUP_ADDRESS)
        dst_raw_address = (cemi[6 + addil], cemi[7 + addil])
        self.dst_addr = (GroupAddress(dst_raw_address) if dst_is_group_address
                         else IndividualAddress(dst_raw_address))

        npdu_len = cemi[8 + addil]

        apdu = cemi[9 + addil:]
        if len(apdu) != (npdu_len +
                         1):  # TCPI octet not included in NPDU length
            raise CouldNotParseKNXIP(
                f"APDU LEN should be {npdu_len} but is {len(apdu) - 1} in CEMI: {cemi.hex()}"
            )

        # TPCI (transport layer control information)
        # - with control bit set -> 8 bit; no APDU
        # - no control bit set (data) -> First 6 bit
        # APCI (application layer control information) -> Last  10 bit of TPCI/APCI
        try:
            self.tpci = TPCI.resolve(raw_tpci=cemi[9 + addil],
                                     dst_is_group_address=dst_is_group_address)
        except ConversionError as err:
            raise UnsupportedCEMIMessage(
                f"TPCI not supported: {cemi[9 + addil]:#10b}") from err

        if self.tpci.control:
            if npdu_len:
                raise UnsupportedCEMIMessage(
                    f"Invalid length for control TPDU {self.tpci}: {npdu_len}")
            return 10 + addil

        _apci = apdu[0] * 256 + apdu[1]
        try:
            self.payload = APCI.resolve_apci(_apci)
        except ConversionError as err:
            raise UnsupportedCEMIMessage(
                f"APCI not supported: {_apci:#012b}") from err

        self.payload.from_knx(apdu)

        return 10 + addil + npdu_len

    def to_knx(self) -> bytes:
        """Serialize to KNX/IP raw data."""
        if self.tpci.control:
            tpdu = bytes([self.tpci.to_knx()])
            npdu_len = 0
        else:
            if not isinstance(self.payload, APCI):
                raise ConversionError(
                    f"Invalid payload set for data TPDU: {self.payload.__class__}"
                )
            tpdu = self.payload.to_knx()
            tpdu[0] |= self.tpci.to_knx()
            npdu_len = self.payload.calculated_length()

        if not isinstance(self.src_addr, IndividualAddress):
            raise ConversionError("src_addr invalid")
        if not isinstance(self.dst_addr, (GroupAddress, IndividualAddress)):
            raise ConversionError("dst_addr invalid")

        return (bytes((
            self.code.value,
            0x00,  # Additional information length
        )) + self.flags.to_bytes(2, "big") + bytes((
            *self.src_addr.to_knx(),
            *self.dst_addr.to_knx(),
            npdu_len,
        )) + tpdu)

    def __repr__(self) -> str:
        """Return object as readable string."""
        return ("<CEMIFrame "
                f'code="{self.code.name}" '
                f'src_addr="{self.src_addr.__repr__()}" '
                f'dst_addr="{self.dst_addr.__repr__()}" '
                f'flags="{self.flags:16b}" '
                f'tpci="{self.tpci}" '
                f'payload="{self.payload}" />')

    def __eq__(self, other: object) -> bool:
        """Equal operator."""
        return self.__dict__ == other.__dict__