Exemplo 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):
        """Initialize DIBDeviceInformation class."""
        super(DIBDeviceInformation, self).__init__()
        self.knx_medium = KNXMedium.TP1
        self.programming_mode = False
        self.individual_address = Address()
        self.installation_number = 0
        self.project_number = 0
        self.serial_number = ""
        self.multicast_address = "224.0.23.12"
        self.mac_address = ""
        self.name = ""

    def calculated_length(self):
        """Get length of KNX/IP object."""
        return DIBDeviceInformation.LENGTH

    def from_knx(self, raw):
        """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 = \
            Address((raw[4], raw[5]), AddressType.PHYSICAL)
        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):
        """Serialize to KNX/IP raw data."""
        def hex_notation_to_knx(serial_number):
            """Serialize hex notation."""
            for part in serial_number.split(":"):
                yield int(part, 16)

        def ip_to_knx(ip_addr):
            """Serialize ip."""
            for part in ip_addr.split("."):
                yield int(part)

        def str_to_knx(string, length):
            """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):
        """Return object as readable string."""
        return '<DIBDeviceInformation ' \
               '\n\tknx_medium="{0}" ' \
               '\n\tprogramming_mode="{1}" ' \
               '\n\tindividual_address="{2}" ' \
               '\n\tinstallation_number="{3}" ' \
               '\n\tproject_number="{4}" ' \
               '\n\tserial_number="{5}" ' \
               '\n\tmulticast_address="{6}" ' \
               '\n\tmac_address="{7}" ' \
               '\n\tname="{8}" />'.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)
Exemplo n.º 2
0
class CEMIFrame(KNXIPBody):
    """Representation of a CEMI Frame."""
    # pylint: disable=too-many-instance-attributes

    def __init__(self):
        """CEMIFrame __init__ object."""
        super(CEMIFrame, self).__init__()
        self.code = CEMIMessageCode.L_DATA_IND
        self.flags = 0
        self.cmd = APCICommand.GROUP_READ
        self.src_addr = Address()
        self.dst_addr = Address()
        self.mpdu_len = 0
        self.payload = None

    @property
    def telegram(self):
        telegram = Telegram()
        telegram.payload = self.payload
        telegram.group_address = self.dst_addr

        def resolve_telegram_type(cmd):
            if cmd == APCICommand.GROUP_WRITE:
                return TelegramType.GROUP_WRITE
            elif cmd == APCICommand.GROUP_READ:
                return TelegramType.GROUP_READ
            elif cmd == APCICommand.GROUP_RESPONSE:
                return TelegramType.GROUP_RESPONSE
            else:
                raise ConversionException("Telegram not implemented for {0}" \
                                      .format(self.cmd))

        telegram.telegramtype = resolve_telegram_type(self.cmd)

        # TODO: Set telegram.direction [additional flag within KNXIP]
        return telegram


    @telegram.setter
    def telegram(self, telegram):

        self.dst_addr = telegram.group_address
        self.payload = telegram.payload

        # 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.DESTINATION_GROUP_ADDRESS |
                      CEMIFlags.HOP_COUNT_1ST)

        # TODO: use telegram.direction
        def resolve_cmd(telegramtype):
            if telegramtype == TelegramType.GROUP_READ:
                return APCICommand.GROUP_READ
            elif telegramtype == TelegramType.GROUP_WRITE:
                return APCICommand.GROUP_WRITE
            elif telegramtype == TelegramType.GROUP_RESPONSE:
                return APCICommand.GROUP_RESPONSE
            else:
                raise TypeError()

        self.cmd = resolve_cmd(telegram.telegramtype)


    def set_hops(self, hops):
        # Resetting hops
        self.flags &= 0xFFFF ^ 0x0070
        # Setting new hops
        self.flags |= hops << 4

    def calculated_length(self):
        if self.payload is None:
            return 11
        elif isinstance(self.payload, DPTBinary):
            return 11
        elif isinstance(self.payload, DPTArray):
            return 11 + len(self.payload.value)
        else:
            raise TypeError()

    def from_knx(self, cemi):
        """Create a new CEMIFrame initialized from the given CEMI data."""
        if len(cemi) < 11:
            raise CouldNotParseKNXIP("CEMI too small")

        self.code = CEMIMessageCode(cemi[0])
        offset = cemi[1]

        self.flags = cemi[2] * 256 + cemi[3]

        self.src_addr = Address((cemi[4 + offset], cemi[5 + offset]), \
                                AddressType.PHYSICAL)

        dst_addr_type = \
            AddressType.GROUP \
            if self.flags & CEMIFlags.DESTINATION_GROUP_ADDRESS \
            else AddressType.PHYSICAL

        self.dst_addr = Address((cemi[6 + offset], cemi[7 + offset]),
                                dst_addr_type)

        self.mpdu_len = cemi[8 + offset]

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

        tpci_apci = cemi[9 + offset] * 256 + cemi[10 + offset]

        self.cmd = APCICommand(tpci_apci & 0xFFC0)

        apdu = cemi[10 + offset:]
        if len(apdu) != self.mpdu_len:
            raise CouldNotParseKNXIP(
                "APDU LEN should be {} but is {}".format(
                    self.mpdu_len, len(apdu)))

        #pylint: disable=redefined-variable-type
        if len(apdu) == 1:
            apci = tpci_apci & DPTBinary.APCI_BITMASK
            self.payload = DPTBinary(apci)
        else:
            self.payload = DPTArray(cemi[11 + offset:])

        return 10 + offset + len(apdu)

    def to_knx(self):
        """Convert the CEMI frame object to its byte representation."""

        if not isinstance(self.src_addr, Address):
            raise ConversionException("src_add not set")
        if not isinstance(self.dst_addr, Address):
            raise ConversionException("dst_add 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())

        def encode_cmd_and_payload(cmd, encoded_payload=0,\
                                   appended_payload=None):
            if appended_payload is None:
                appended_payload = []
            data = [
                1 + len(appended_payload),
                (cmd.value >> 8) & 0xff,
                (cmd.value & 0xff) |
                (encoded_payload & DPTBinary.APCI_BITMASK)]
            data.extend(appended_payload)
            return data

        if self.payload is None:
            data.extend(encode_cmd_and_payload(self.cmd))
        elif isinstance(self.payload, DPTBinary):
            data.extend(encode_cmd_and_payload(self.cmd, \
                        encoded_payload=self.payload.value))
        elif isinstance(self.payload, DPTArray):
            data.extend(encode_cmd_and_payload(self.cmd, \
                        appended_payload=self.payload.value))
        else:
            raise TypeError()

        return data


    def __str__(self):
        return "<CEMIFrame SourceAddress={0}, DestinationAddress={1}, " \
               "Flags={2:16b} Command={3}, payload={4}>".format(
                   self.src_addr,
                   self.dst_addr,
                   self.flags,
                   self.cmd,
                   self.payload)
Exemplo n.º 3
0
class CEMIFrame(KNXIPBody):
    """Representation of a CEMI Frame."""

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

    def __init__(self, xknx):
        """Initialize CEMIFrame object."""
        super(CEMIFrame, self).__init__(xknx)
        self.code = CEMIMessageCode.L_DATA_IND
        self.flags = 0
        self.cmd = APCICommand.GROUP_READ
        self.src_addr = Address()
        self.dst_addr = Address()
        self.mpdu_len = 0
        self.payload = None

    @property
    def telegram(self):
        """Return telegram."""
        telegram = Telegram()
        telegram.payload = self.payload
        telegram.group_address = self.dst_addr

        def resolve_telegram_type(cmd):
            """Return telegram type from APCI Command."""
            if cmd == APCICommand.GROUP_WRITE:
                return TelegramType.GROUP_WRITE
            elif cmd == APCICommand.GROUP_READ:
                return TelegramType.GROUP_READ
            elif cmd == APCICommand.GROUP_RESPONSE:
                return TelegramType.GROUP_RESPONSE
            else:
                raise ConversionError(
                    "Telegram not implemented for {0}".format(self.cmd))

        telegram.telegramtype = resolve_telegram_type(self.cmd)

        # TODO: Set telegram.direction [additional flag within KNXIP]
        return telegram

    @telegram.setter
    def telegram(self, telegram):
        """Set telegram."""
        self.dst_addr = telegram.group_address
        self.payload = telegram.payload

        # 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.DESTINATION_GROUP_ADDRESS
                      | CEMIFlags.HOP_COUNT_1ST)

        # TODO: use telegram.direction
        def resolve_cmd(telegramtype):
            """Resolve APCICommand from TelegramType."""
            if telegramtype == TelegramType.GROUP_READ:
                return APCICommand.GROUP_READ
            elif telegramtype == TelegramType.GROUP_WRITE:
                return APCICommand.GROUP_WRITE
            elif telegramtype == TelegramType.GROUP_RESPONSE:
                return APCICommand.GROUP_RESPONSE
            else:
                raise TypeError()

        self.cmd = resolve_cmd(telegram.telegramtype)

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

    def calculated_length(self):
        """Get length of KNX/IP body."""
        if self.payload is None:
            return 11
        elif isinstance(self.payload, DPTBinary):
            return 11
        elif isinstance(self.payload, DPTArray):
            return 11 + len(self.payload.value)
        else:
            raise TypeError()

    def from_knx(self, raw):
        """Parse/deserialize from KNX/IP raw data."""
        self.code = CEMIMessageCode(raw[0])

        if self.code == CEMIMessageCode.L_DATA_IND or \
                self.code == CEMIMessageCode.L_Data_REQ or \
                self.code == CEMIMessageCode.L_DATA_CON:
            return self.from_knx_data_link_layer(raw)
        else:
            raise CouldNotParseKNXIP(
                "Could not understand CEMIMessageCode: {0} / {1}".format(
                    self.code, raw[0]))

    def from_knx_data_link_layer(self, cemi):
        """Parse L_DATA_IND, CEMIMessageCode.L_Data_REQ, CEMIMessageCode.L_DATA_CON."""
        if len(cemi) < 11:
            raise CouldNotParseKNXIP("CEMI too small")

        # 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]

        self.flags = cemi[2 + addil] * 256 + cemi[3 + addil]

        self.src_addr = Address((cemi[4 + addil], cemi[5 + addil]),
                                AddressType.PHYSICAL)

        if self.flags & CEMIFlags.DESTINATION_GROUP_ADDRESS:
            self.dst_addr = Address((cemi[6 + addil], cemi[7 + addil]),
                                    address_type=AddressType.GROUP,
                                    address_format=self.xknx.address_format)
        else:
            self.dst_addr = Address((cemi[6 + addil], cemi[7 + addil]),
                                    address_type=AddressType.PHYSICAL)

        self.mpdu_len = cemi[8 + addil]

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

        tpci_apci = cemi[9 + addil] * 256 + cemi[10 + addil]

        self.cmd = APCICommand(tpci_apci & 0xFFC0)

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

        if len(apdu) == 1:
            apci = tpci_apci & DPTBinary.APCI_BITMASK
            self.payload = DPTBinary(apci)
        else:
            self.payload = DPTArray(cemi[11 + addil:])

        return 10 + addil + len(apdu)

    def to_knx(self):
        """Serialize to KNX/IP raw data."""
        if not isinstance(self.src_addr, Address):
            raise ConversionError("src_add not set")
        if not isinstance(self.dst_addr, Address):
            raise ConversionError("dst_add 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())

        def encode_cmd_and_payload(cmd,
                                   encoded_payload=0,
                                   appended_payload=None):
            """Encode cmd and payload."""
            if appended_payload is None:
                appended_payload = []
            data = [
                1 + len(appended_payload), (cmd.value >> 8) & 0xff,
                (cmd.value & 0xff) | (encoded_payload & DPTBinary.APCI_BITMASK)
            ]
            data.extend(appended_payload)
            return data

        if self.payload is None:
            data.extend(encode_cmd_and_payload(self.cmd))
        elif isinstance(self.payload, DPTBinary):
            data.extend(
                encode_cmd_and_payload(self.cmd,
                                       encoded_payload=self.payload.value))
        elif isinstance(self.payload, DPTArray):
            data.extend(
                encode_cmd_and_payload(self.cmd,
                                       appended_payload=self.payload.value))
        else:
            raise TypeError()
        return data

    def __str__(self):
        """Return object as readable string."""
        return '<CEMIFrame SourceAddress="{0}" DestinationAddress="{1}" ' \
               'Flags="{2:16b}" Command="{3}" payload="{4}" />'.format(
                   self.src_addr,
                   self.dst_addr,
                   self.flags,
                   self.cmd,
                   self.payload)