Repository: XKNX/xknx
Pull request #533 (2020-12-16 00:32:12)

Extend APCI services
## Description
This extends #519 to support more APCI services. Basically, this parses (or generates) other payloads for telegrams.

I've added two additional examples (`example_info.py` and `example_restart.py`) to demonstrate some new APCI services. The existings `example_telegram_monitor.py` should be able to parse telegrams with these APCI services.

Note that the KNX application layer specification defines when to send specific APCI services. For instance, `UserMemoryRead` should only be send in a point-to-point connection-oriented link (which is not supported by XKNX). But it is still possible to generate and send the payloads, e.g. for your own hardware :-)

## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


coveralls: 
[![Coverage Status](https://coveralls.io/builds/35745770/badge)](https://coveralls.io/builds/35745770)

Coverage decreased (-0.6%) to 94.162% when pulling **549d019aea87ab20dd404fac51f0029bbb77ee7f on basilfx:feature/apci_payload_extended** into **339f526fc82f646414b1bc8cddbb8896d761a31b on XKNX:master**.


Patch:
diff --git a/changelog.md b/changelog.md
index 6d1a4d67..88560396 100644
--- a/changelog.md
+++ b/changelog.md
@@ -22,6 +22,7 @@
 - Telegram: `group_address` renamed to `destination_address`, to prepare support for other APCI services and add `source_address`
 - Telegram: remove `Telegram.telegramtype` and replace with payload object derived from `xknx.telegram.apci.APCI`.
 - CEMIFrame: remove `CEMIFrame.cmd`, which can be derived from `CEMIFrame.payload`.
+- APCI: extend APCI services (e.g. `MemoryRead/Write/Response`, `PropertyRead/Write/Response`, etc).
 - Farewell Travis CI; Welcome Github Actions!
 - StateUpdater allow float values for `register_remote_value(tracker_options)` attribute.
 - Handle exceptions from received unsupported or not implemented KNXIP Service Type identifiers
diff --git a/examples/example_info.py b/examples/example_info.py
new file mode 100644
index 00000000..ba84bc68
--- /dev/null
+++ b/examples/example_info.py
@@ -0,0 +1,65 @@
+"""Example on how to read mask version and properties from a KNX actor."""
+import asyncio
+import sys
+from typing import List
+
+from xknx import XKNX
+from xknx.core import PayloadReader
+from xknx.telegram import IndividualAddress
+from xknx.telegram.apci import (
+    DeviceDescriptorRead,
+    DeviceDescriptorResponse,
+    PropertyValueRead,
+    PropertyValueResponse,
+)
+
+
+async def main(argv: List[str]):
+    """Connect and read information from a KNX device. Requires a System B device."""
+    if len(argv) == 2:
+        address = IndividualAddress(argv[1])
+    else:
+        address = "1.1.1"
+
+    xknx = XKNX()
+    await xknx.start()
+
+    reader = PayloadReader(xknx, address)
+
+    # Read the mask version of the device (descriptor 0).
+    payload = await reader.send(
+        DeviceDescriptorRead(descriptor=0), response_class=DeviceDescriptorResponse
+    )
+    if payload is not None:
+        print("Mask version: %04x" % payload.value)
+
+    # Read the serial number of the device (object 0, property 11).
+    payload = await reader.send(
+        PropertyValueRead(object_index=0, property_id=11, count=1, start_index=1),
+        response_class=PropertyValueResponse,
+    )
+    if payload is not None:
+        print(
+            "Serial number: {:02x}{:02x}:{:02x}{:02x}{:02x}{:02x}".format(
+                payload.data[0],
+                payload.data[1],
+                payload.data[2],
+                payload.data[3],
+                payload.data[4],
+                payload.data[5],
+            )
+        )
+
+    # Check if the device is in programming mode (object 0, property 54).
+    payload = await reader.send(
+        PropertyValueRead(object_index=0, property_id=54, count=1, start_index=1),
+        response_class=PropertyValueResponse,
+    )
+    if payload is not None:
+        print("Programming mode: %s" % ("ON" if payload.data[0] else "OFF"))
+
+    await xknx.stop()
+
+
+if __name__ == "__main__":
+    asyncio.run(main(sys.argv))
diff --git a/examples/example_restart.py b/examples/example_restart.py
new file mode 100644
index 00000000..4e6e38a0
--- /dev/null
+++ b/examples/example_restart.py
@@ -0,0 +1,29 @@
+"""Example on how to connect to restart a KNX device."""
+import asyncio
+import sys
+from typing import List
+
+from xknx import XKNX
+from xknx.telegram import IndividualAddress, Telegram
+from xknx.telegram.apci import Restart
+
+
+async def main(argv: List[str]):
+    """Restart a KNX device."""
+    if len(argv) != 2:
+        print(f"{argv[0]}: missing target address.")
+        return 1
+
+    address = IndividualAddress(argv[1])
+
+    xknx = XKNX()
+    await xknx.start()
+
+    await xknx.telegrams.put(Telegram(address, payload=Restart()))
+    await asyncio.sleep(2)
+
+    await xknx.stop()
+
+
+if __name__ == "__main__":
+    asyncio.run(main(sys.argv))
diff --git a/examples/readme.md b/examples/readme.md
index 30ce8d0e..e65370e5 100644
--- a/examples/readme.md
+++ b/examples/readme.md
@@ -26,4 +26,11 @@
 |[Sensor](./example_sensor.py)|Example for Sensor device|
 |[Switch](./example_switch.py)|Example for Switch device|
 |[Scene](./example_scene.py)|Example for switching a light on and off|
-|[Weather](./example_weather.py)|Example for Weather device and reading sensor data|
\ No newline at end of file
+|[Weather](./example_weather.py)|Example for Weather device and reading sensor data|
+
+## Low-level
+
+|Example|Description|
+|-|-|
+|[Info](./example_info.py)|Example on how to read device information such as serial and programming mode (depends on actor if supported)|
+|[Restart](./example_restart.py)|Example on how to restart an actor|
\ No newline at end of file
diff --git a/xknx/core/__init__.py b/xknx/core/__init__.py
index a8b0d73c..efb08a54 100644
--- a/xknx/core/__init__.py
+++ b/xknx/core/__init__.py
@@ -1,5 +1,6 @@
 """Module for the automations and business logic of XKNX."""
 # flake8: noqa
+from .payload_reader import PayloadReader
 from .state_updater import StateUpdater
 from .telegram_queue import TelegramQueue
 from .value_reader import ValueReader
diff --git a/xknx/core/payload_reader.py b/xknx/core/payload_reader.py
new file mode 100644
index 00000000..85f05df6
--- /dev/null
+++ b/xknx/core/payload_reader.py
@@ -0,0 +1,102 @@
+"""
+Module for sending and receiving arbitrary payloads from the KNX bus.
+
+The module will
+* ... send the payload to the selected address.
+* ... register a callback for receiving telegrams within telegram queue.
+* ... check if received telegrams have the correct type and address.
+* ... store the received telegram for further processing.
+"""
+import asyncio
+import logging
+from typing import TYPE_CHECKING, Optional, Type, Union
+
+from xknx.telegram import GroupAddress, IndividualAddress, Telegram
+from xknx.telegram.apci import APCI
+
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+logger = logging.getLogger("xknx.log")
+
+
+class PayloadReader:
+    """Class for sending a request and waiting for a response on the KNX bus."""
+
+    def __init__(
+        self,
+        xknx: "XKNX",
+        address: Union[GroupAddress, IndividualAddress],
+        timeout_in_seconds: float = 2.0,
+    ) -> None:
+        """Initialize PayloadReader class."""
+        self.xknx = xknx
+        self.address = address
+        self.response_received_or_timeout = asyncio.Event()
+        self.success: bool = False
+        self.timeout_in_seconds = timeout_in_seconds
+        self.received_payload: Optional[APCI] = None
+
+    def reset(self) -> None:
+        """Reset reader for next send."""
+        self.response_received_or_timeout = asyncio.Event()
+        self.success = False
+        self.received_payload = None
+
+    async def send(
+        self, payload: APCI, response_class: Optional[Type[APCI]] = None
+    ) -> Optional[APCI]:
+        """
+        Send APCI payload request and wait for a response.
+
+        An optional `response_class` can be specified to wait for a specific
+        APCI response payload.
+        """
+        self.reset()
+
+        cb_obj = self.xknx.telegram_queue.register_telegram_received_cb(
+            lambda t: self.telegram_received(t, response_class)
+        )
+        await self.send_telegram(payload)
+
+        try:
+            await asyncio.wait_for(
+                self.response_received_or_timeout.wait(),
+                timeout=self.timeout_in_seconds,
+            )
+        except asyncio.TimeoutError:
+            logger.warning(
+                "Error: KNX bus did not respond in time (%s secs) to payload request for: %s",
+                self.timeout_in_seconds,
+                self.address,
+            )
+        finally:
+            # cleanup to not leave callbacks (for asyncio.CancelledError)
+            self.xknx.telegram_queue.unregister_telegram_received_cb(cb_obj)
+
+        if not self.success:
+            return None
+        if self.received_payload is None:
+            return None
+        return self.received_payload
+
+    async def send_telegram(self, payload: APCI) -> None:
+        """Send the telegram."""
+        await self.xknx.telegrams.put(
+            Telegram(destination_address=self.address, payload=payload)
+        )
+
+    async def telegram_received(
+        self, telegram: Telegram, response_class: Optional[Type[APCI]] = None
+    ) -> None:
+        """Test if telegram is of correct type and address, then trigger event."""
+        if self.address != telegram.source_address:
+            return
+        if not isinstance(telegram.payload, APCI):
+            return
+        if response_class:
+            if not isinstance(telegram.payload, response_class):
+                return
+        self.success = True
+        self.received_payload = telegram.payload
+        self.response_received_or_timeout.set()
diff --git a/xknx/telegram/apci.py b/xknx/telegram/apci.py
index c5075ba2..1e9d17c5 100644
--- a/xknx/telegram/apci.py
+++ b/xknx/telegram/apci.py
@@ -8,14 +8,16 @@ is a service that takes a DPT as a value.
 """
 from abc import ABC, abstractmethod
 from enum import Enum
+import struct
 from typing import ClassVar, Optional, Union, cast
 
 from xknx.dpt import DPTArray, DPTBinary
 from xknx.exceptions import ConversionError
+from xknx.telegram.address import IndividualAddress
 
 
 def encode_cmd_and_payload(
-    cmd: "APCICommand",
+    cmd: Union["APCIService", "APCIUserService", "APCIExtendedService"],
     encoded_payload: int = 0,
     appended_payload: Optional[bytes] = None,
 ) -> bytes:
@@ -33,19 +35,66 @@ def encode_cmd_and_payload(
     return data
 
 
-class APCICommand(Enum):
+class APCIService(Enum):
     """Enum class for APCI services."""
 
     GROUP_READ = 0x0000
     GROUP_RESPONSE = 0x0040
     GROUP_WRITE = 0x0080
 
+    INDIVIDUAL_ADDRESS_WRITE = 0x00C0
+    INDIVIDUAL_ADDRESS_READ = 0x0100
+    INDIVIDUAL_ADDRESS_RESPONSE = 0x140
+
+    ADC_READ = 0x0180
+    ADC_RESPONSE = 0x1C0
+
+    MEMORY_READ = 0x0200
+    MEMORY_RESPONSE = 0x0240
+    MEMORY_WRITE = 0x0280
+
+    USER_MESSAGE = 0x02C0
+
+    DEVICE_DESCRIPTOR_READ = 0x0300
+    DEVICE_DESCRIPTOR_RESPONSE = 0x0340
+
+    RESTART = 0x0380
+
     ESCAPE = 0x03C0
 
 
-class APCIExtendedCommand(Enum):
+class APCIUserService(Enum):
+    """Enum class for user message APCI services."""
+
+    USER_MEMORY_READ = 0x02C0
+    USER_MEMORY_RESPONSE = 0x02C1
+    USER_MEMORY_WRITE = 0x02C2
+
+    USER_MANUFACTURER_INFO_READ = 0x02C5
+    USER_MANUFACTURER_INFO_RESPONSE = 0x02C6
+
+    FUNCTION_PROPERTY_COMMAND = 0x02C7
+    FUNCTION_PROPERTY_STATE_READ = 0x02C8
+    FUNCTION_PROPERTY_STATE_RESPONSE = 0x02C9
+
+
+class APCIExtendedService(Enum):
     """Enum class for extended APCI services."""
 
+    AUTHORIZE_REQUEST = 0x03D1
+    AUTHORIZE_RESPONSE = 0x03D2
+
+    PROPERTY_VALUE_READ = 0x03D5
+    PROPERTY_VALUE_RESPONSE = 0x03D6
+    PROPERTY_VALUE_WRITE = 0x03D7
+
+    PROPERTY_DESCRIPTION_READ = 0x03D8
+    PROPERTY_DESCRIPTION_RESPONSE = 0x03D9
+
+    INDIVIDUAL_ADDRESS_SERIAL_READ = 0x03DC
+    INDIVIDUAL_ADDRESS_SERIAL_RESPONSE = 0x03DD
+    INDIVIDUAL_ADDRESS_SERIAL_WRITE = 0x03DE
+
 
 class APCI(ABC):
     """
@@ -54,7 +103,9 @@ class APCI(ABC):
     This base class is only the interface for the derived classes.
     """
 
-    code: ClassVar[APCICommand] = cast(APCICommand, None)
+    code: ClassVar[Union[APCIService, APCIUserService, APCIExtendedService]] = cast(
+        APCIService, None
+    )
 
     @abstractmethod
     def calculated_length(self) -> int:
@@ -74,22 +125,82 @@ class APCI(ABC):
 
     @staticmethod
     def resolve_apci(apci: int) -> "APCI":
-        """Return APCI instance from APCI command."""
-        extended = (apci & APCICommand.ESCAPE.value) == APCICommand.ESCAPE.value
+        """
+        Return APCI instance from APCI command.
 
-        if extended:
-            raise ConversionError(
-                f"Class not implemented for extended APCI {apci:#012b}."
-            )
-
-        apci = apci & 0x03C0
+        There are only 16 possible APCI services. The
+        `APCIService.USER_MESSAGE` and `APCIService.ESCAPE` service have
+        several sub-services.
+        """
+        service = apci & 0x03C0
 
-        if apci == APCICommand.GROUP_READ.value:
+        if service == APCIService.GROUP_READ.value:
             return GroupValueRead()
-        if apci == APCICommand.GROUP_WRITE.value:
+        if service == APCIService.GROUP_WRITE.value:
             return GroupValueWrite()
-        if apci == APCICommand.GROUP_RESPONSE.value:
+        if service == APCIService.GROUP_RESPONSE.value:
             return GroupValueResponse()
+        if service == APCIService.INDIVIDUAL_ADDRESS_WRITE.value:
+            return IndividualAddressWrite()
+        if service == APCIService.INDIVIDUAL_ADDRESS_READ.value:
+            return IndividualAddressRead()
+        if service == APCIService.INDIVIDUAL_ADDRESS_RESPONSE.value:
+            return IndividualAddressResponse()
+        if service == APCIService.ADC_READ.value:
+            return ADCRead()
+        if service == APCIService.ADC_RESPONSE.value:
+            return ADCResponse()
+        if service == APCIService.MEMORY_READ.value:
+            return MemoryRead()
+        if service == APCIService.MEMORY_WRITE.value:
+            return MemoryWrite()
+        if service == APCIService.MEMORY_RESPONSE.value:
+            return MemoryResponse()
+        if service == APCIService.USER_MESSAGE.value:
+            if apci == APCIUserService.USER_MEMORY_READ.value:
+                return UserMemoryRead()
+            if apci == APCIUserService.USER_MEMORY_RESPONSE.value:
+                return UserMemoryResponse()
+            if apci == APCIUserService.USER_MEMORY_WRITE.value:
+                return UserMemoryWrite()
+            if apci == APCIUserService.USER_MANUFACTURER_INFO_READ.value:
+                return UserManufacturerInfoRead()
+            if apci == APCIUserService.USER_MANUFACTURER_INFO_RESPONSE.value:
+                return UserManufacturerInfoResponse()
+            if apci == APCIUserService.FUNCTION_PROPERTY_COMMAND.value:
+                return FunctionPropertyCommand()
+            if apci == APCIUserService.FUNCTION_PROPERTY_STATE_READ.value:
+                return FunctionPropertyStateRead()
+            if apci == APCIUserService.FUNCTION_PROPERTY_STATE_RESPONSE.value:
+                return FunctionPropertyStateResponse()
+        if service == APCIService.DEVICE_DESCRIPTOR_READ.value:
+            return DeviceDescriptorRead()
+        if service == APCIService.DEVICE_DESCRIPTOR_RESPONSE.value:
+            return DeviceDescriptorResponse()
+        if service == APCIService.RESTART.value:
+            return Restart()
+        if service == APCIService.ESCAPE.value:
+            if apci == APCIExtendedService.AUTHORIZE_REQUEST.value:
+                return AuthorizeRequest()
+            if apci == APCIExtendedService.AUTHORIZE_RESPONSE.value:
+                return AuthorizeResponse()
+            if apci == APCIExtendedService.PROPERTY_VALUE_READ.value:
+                return PropertyValueRead()
+            if apci == APCIExtendedService.PROPERTY_VALUE_WRITE.value:
+                return PropertyValueWrite()
+            if apci == APCIExtendedService.PROPERTY_VALUE_RESPONSE.value:
+                return PropertyValueResponse()
+            if apci == APCIExtendedService.PROPERTY_DESCRIPTION_READ.value:
+                return PropertyDescriptionRead()
+            if apci == APCIExtendedService.PROPERTY_DESCRIPTION_RESPONSE.value:
+                return PropertyDescriptionResponse()
+            if apci == APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_READ.value:
+                return IndividualAddressSerialRead()
+            if apci == APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_RESPONSE.value:
+                return IndividualAddressSerialResponse()
+            if apci == APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_WRITE.value:
+                return IndividualAddressSerialWrite()
+
         raise ConversionError(f"Class not implemented for APCI {apci:#012b}.")
 
 
@@ -100,7 +211,7 @@ class GroupValueRead(APCI):
     Does not have any payload.
     """
 
-    code = APCICommand.GROUP_READ
+    code = APCIService.GROUP_READ
 
     def calculated_length(self) -> int:
         """Get length of APCI payload."""
@@ -128,7 +239,7 @@ class GroupValueWrite(APCI):
     Takes a value (DPT) as payload.
     """
 
-    code = APCICommand.GROUP_WRITE
+    code = APCIService.GROUP_WRITE
 
     def __init__(self, value: Optional[Union[DPTBinary, DPTArray]] = None) -> None:
         """Initialize a new instance of GroupValueWrite."""
@@ -171,7 +282,7 @@ class GroupValueResponse(APCI):
     Takes a value (DPT) as payload.
     """
 
-    code = APCICommand.GROUP_RESPONSE
+    code = APCIService.GROUP_RESPONSE
 
     def __init__(self, value: Optional[Union[DPTBinary, DPTArray]] = None) -> None:
         """Initialize a new instance of GroupValueResponse."""
@@ -205,3 +316,1263 @@ class GroupValueResponse(APCI):
     def __str__(self) -> str:
         """Return object as readable string."""
         return f'<GroupValueResponse value="{self.value}" />'
+
+
+class IndividualAddressWrite(APCI):
+    """
+    IndividualAddressWrite service.
+
+    Payload contains the serial number and (new) address of the device.
+    """
+
+    code = APCIService.INDIVIDUAL_ADDRESS_WRITE
+
+    def __init__(
+        self,
+        address: Optional[IndividualAddress] = None,
+    ) -> None:
+        """Initialize a new instance of IndividualAddressWrite."""
+        if address is None:
+            address = IndividualAddress("0.0.0")
+
+        self.address = address
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        address_high, address_low = struct.unpack("!BB", raw[2:])
+
+        self.address = IndividualAddress((address_high, address_low))
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return encode_cmd_and_payload(
+            self.code, appended_payload=bytes(self.address.to_knx())
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<IndividualAddressWrite address="{self.address}" />'
+
+
+class IndividualAddressRead(APCI):
+    """IndividualAddressRead service."""
+
+    code = APCIService.INDIVIDUAL_ADDRESS_READ
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 1
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+
+        # Nothing to parse, but must be implemented explicitly.
+        return
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return encode_cmd_and_payload(self.code)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return "<IndividualAddressRead />"
+
+
+class IndividualAddressResponse(APCI):
+    """
+    IndividualAddressResponse service.
+
+    There is no payload, since the Telegram's source address is used as a
+    response address.
+    """
+
+    code = APCIService.INDIVIDUAL_ADDRESS_RESPONSE
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 1
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+
+        # Nothing to parse, but must be implemented explicitly.
+        return
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return encode_cmd_and_payload(self.code)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return "<IndividualAddressResponse />"
+
+
+class ADCRead(APCI):
+    """
+    ADCRead service.
+
+    Payload contains the channel and number of samples to take.
+    """
+
+    code = APCIService.ADC_READ
+
+    def __init__(self, channel: int = 0, count: int = 0) -> None:
+        """Initialize a new instance of ADCRead."""
+        self.channel = channel
+        self.count = count
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 2
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        channel, self.count = struct.unpack("!BB", raw[1:])
+
+        self.channel = channel & DPTBinary.APCI_BITMASK
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        payload = struct.pack("!BB", self.channel, self.count)
+
+        return encode_cmd_and_payload(
+            self.code, encoded_payload=payload[0], appended_payload=payload[1:]
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<ADCRead channel="{self.channel}" count="{self.count}" />'
+
+
+class ADCResponse(APCI):
+    """
+    ADCResponse service.
+
+    Payload contains the channel, number of samples and value.
+    """
+
+    code = APCIService.ADC_RESPONSE
+
+    def __init__(self, channel: int = 0, count: int = 0, value: int = 0) -> None:
+        """Initialize a new instance of ADCResponse."""
+        self.channel = channel
+        self.count = count
+        self.value = value
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        channel, self.count, self.value = struct.unpack("!BBH", raw[1:])
+
+        self.channel = channel & DPTBinary.APCI_BITMASK
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        payload = struct.pack("!BBH", self.channel, self.count, self.value)
+
+        return encode_cmd_and_payload(
+            self.code, encoded_payload=payload[0], appended_payload=payload[1:]
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<ADCResponse channel="{self.channel}" count="{self.count}" value="{self.value}" />'
+
+
+class MemoryRead(APCI):
+    """
+    MemoryRead service.
+
+    Payload indicates address (64 KiB) and count (1-63 bytes).
+    """
+
+    code = APCIService.MEMORY_READ
+
+    def __init__(self, address: int = 0, count: int = 0) -> None:
+        """Initialize a new instance of MemoryRead."""
+        self.address = address
+        self.count = count
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        count, self.address = struct.unpack("!BH", raw[1:])
+
+        self.count = count & DPTBinary.APCI_BITMASK
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.address < 0 or self.address >= 2 ** 16:
+            raise ConversionError("Address out of range.")
+        if self.count < 0 or self.count >= 2 ** 6:
+            raise ConversionError("Count out of range.")
+
+        payload = struct.pack("!BH", self.count, self.address)
+
+        return encode_cmd_and_payload(
+            self.code, encoded_payload=payload[0], appended_payload=payload[1:]
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<MemoryRead address="{hex(self.address)}" count="{self.count}" />'
+
+
+class MemoryWrite(APCI):
+    """
+    MemoryWrite service.
+
+    Payload indicates address (64 KiB), count (1-63 bytes) and data.
+    """
+
+    code = APCIService.MEMORY_WRITE
+
+    def __init__(
+        self, address: int = 0, count: int = 0, data: Optional[bytes] = None
+    ) -> None:
+        """Initialize a new instance of MemoryWrite."""
+
+        if data is None:
+            data = bytearray()
+
+        self.address = address
+        self.count = count
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 4
+
+        count, self.address, self.data = struct.unpack(f"!BH{size}s", raw[1:])
+
+        self.count = count & DPTBinary.APCI_BITMASK
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.address < 0 or self.address >= 2 ** 16:
+            raise ConversionError("Address out of range.")
+        if self.count < 0 or self.count >= 2 ** 6:
+            raise ConversionError("Count out of range.")
+
+        size = len(self.data)
+        payload = struct.pack(f"!BH{size}s", self.count, self.address, self.data)
+
+        return encode_cmd_and_payload(
+            self.code, encoded_payload=payload[0], appended_payload=payload[1:]
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<MemoryWrite address="{hex(self.address)}" count="{self.count}" data="{self.data.hex()}" />'
+
+
+class MemoryResponse(APCI):
+    """
+    MemoryResponse service.
+
+    Payload indicates address (64 KiB), count (1-63 bytes) and data.
+    """
+
+    code = APCIService.MEMORY_RESPONSE
+
+    def __init__(
+        self, address: int = 0, count: int = 0, data: Optional[bytes] = None
+    ) -> None:
+        """Initialize a new instance of MemoryResponse."""
+
+        if data is None:
+            data = bytearray()
+
+        self.address = address
+        self.count = count
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 4
+
+        count, self.address, self.data = struct.unpack(f"!BH{size}s", raw[1:])
+
+        self.count = count & DPTBinary.APCI_BITMASK
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.address < 0 or self.address >= 2 ** 16:
+            raise ConversionError("Address out of range.")
+        if self.count < 0 or self.count >= 2 ** 6:
+            raise ConversionError("Count out of range.")
+
+        size = len(self.data)
+        payload = struct.pack(f"!BH{size}s", self.count, self.address, self.data)
+
+        return encode_cmd_and_payload(
+            self.code, encoded_payload=payload[0], appended_payload=payload[1:]
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<MemoryResponse address="{hex(self.address)}" count="{self.count}" data="{self.data.hex()}" />'
+
+
+class DeviceDescriptorRead(APCI):
+    """
+    DeviceDescriptorRead service.
+
+    Payload contains the descriptor.
+    """
+
+    code = APCIService.DEVICE_DESCRIPTOR_READ
+
+    def __init__(self, descriptor: int = 0) -> None:
+        """Initialize a new instance of DeviceDescriptorRead."""
+        self.descriptor = descriptor
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 1
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        self.descriptor = raw[1] & 0x3F
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.descriptor < 0 or self.descriptor >= 2 ** 6:
+            raise ConversionError("Descriptor out of range.")
+
+        return encode_cmd_and_payload(self.code, encoded_payload=self.descriptor)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<DeviceDescriptorRead descriptor="{self.descriptor}" />'
+
+
+class DeviceDescriptorResponse(APCI):
+    """
+    DeviceDescriptorResponse service.
+
+    Payload contains the descriptor and value.
+    """
+
+    code = APCIService.DEVICE_DESCRIPTOR_RESPONSE
+
+    def __init__(self, descriptor: int = 0, value: int = 0) -> None:
+        """Initialize a new instance of DeviceDescriptorResponse."""
+        self.descriptor = descriptor
+        self.value = value
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        self.descriptor, self.value = struct.unpack("!BH", raw[1:])
+
+        self.descriptor = self.descriptor & 0x3F
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.descriptor < 0 or self.descriptor >= 2 ** 6:
+            raise ConversionError("Descriptor out of range.")
+
+        payload = struct.pack("!H", self.value)
+
+        return encode_cmd_and_payload(
+            self.code, encoded_payload=self.descriptor, appended_payload=payload
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return '<DeviceDescriptorResponse descriptor="{}" value="{}" />'.format(
+            self.descriptor, self.value
+        )
+
+
+class Restart(APCI):
+    """
+    Restart service.
+
+    Does not take any payload.
+    """
+
+    code = APCIService.RESTART
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 1
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+
+        # Nothing to parse, but must be implemented explicitly.
+        return
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return encode_cmd_and_payload(self.code)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return "<Restart />"
+
+
+class UserMemoryRead(APCI):
+    """
+    UserMemoryRead service.
+
+    Payload indicates address (1 MiB) and count (1-15 bytes).
+    """
+
+    code = APCIUserService.USER_MEMORY_READ
+
+    def __init__(self, address: int = 0, count: int = 0) -> None:
+        """Initialize a new instance of UserMemoryRead."""
+        self.address = address
+        self.count = count
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        byte0, address = struct.unpack("!BH", raw[2:])
+
+        self.count = byte0 & 0x0F
+        self.address = (((byte0 & 0xF0) >> 4) << 16) + address
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.address < 0 or self.address >= 2 ** 20:
+            raise ConversionError("Address out of range.")
+        if self.count < 0 or self.count >= 2 ** 4:
+            raise ConversionError("Count out of range.")
+
+        byte0 = (((self.address & 0x0F0000) >> 16) << 4) | (self.count & 0x0F)
+        address = self.address & 0xFFFF
+
+        payload = struct.pack("!BH", byte0, address)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<UserMemoryRead address="{hex(self.address)}" count="{self.count}" />'
+
+
+class UserMemoryWrite(APCI):
+    """
+    UserMemoryWrite service.
+
+    Payload indicates address (1 MiB), count and data.
+    """
+
+    code = APCIUserService.USER_MEMORY_WRITE
+
+    def __init__(
+        self, address: int = 0, count: int = 0, data: Optional[bytes] = None
+    ) -> None:
+        """Initialize a new instance of UserMemoryWrite."""
+
+        if data is None:
+            data = bytearray()
+
+        self.address = address
+        self.count = count
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 5
+
+        byte0, address, self.data = struct.unpack(f"!BH{size}s", raw[2:])
+
+        self.count = byte0 & 0x0F
+        self.address = (((byte0 & 0xF0) >> 4) << 16) + address
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.address < 0 or self.address >= 2 ** 20:
+            raise ConversionError("Address out of range.")
+        if self.count < 0 or self.count >= 2 ** 4:
+            raise ConversionError("Count out of range.")
+
+        byte0 = (((self.address & 0x0F0000) >> 16) << 4) | (self.count & 0x0F)
+        address = self.address & 0xFFFF
+
+        size = len(self.data)
+        payload = struct.pack(f"!BH{size}s", byte0, address, self.data)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<UserMemoryWrite address="{hex(self.address)}" count="{self.count}" data="{self.data.hex()}" />'
+
+
+class UserMemoryResponse(APCI):
+    """
+    UserMemoryResponse service.
+
+    Payload indicates address (1 MiB), count and data.
+    """
+
+    code = APCIUserService.USER_MEMORY_RESPONSE
+
+    def __init__(
+        self, address: int = 0, count: int = 0, data: Optional[bytes] = None
+    ) -> None:
+        """Initialize a new instance of UserMemoryResponse."""
+
+        if data is None:
+            data = bytearray()
+
+        self.address = address
+        self.count = count
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 5
+
+        byte0, address, self.data = struct.unpack(f"!BH{size}s", raw[2:])
+
+        self.count = byte0 & 0x0F
+        self.address = (((byte0 & 0xF0) >> 4) << 16) + address
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.address < 0 or self.address >= 2 ** 20:
+            raise ConversionError("Address out of range.")
+        if self.count < 0 or self.count >= 2 ** 4:
+            raise ConversionError("Count out of range.")
+
+        byte0 = (((self.address & 0x0F0000) >> 16) << 4) | (self.count & 0x0F)
+        address = self.address & 0xFFFF
+
+        size = len(self.data)
+        payload = struct.pack(f"!BH{size}s", byte0, address, self.data)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<UserMemoryResponse address="{hex(self.address)}" count="{self.count}" data="{self.data.hex()}" />'
+
+
+class UserManufacturerInfoRead(APCI):
+    """UserManufacturerInfoRead service."""
+
+    code = APCIUserService.USER_MANUFACTURER_INFO_READ
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 1
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+
+        # Nothing to parse, but must be implemented explicitly.
+        return
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+
+        return encode_cmd_and_payload(self.code)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return "<UserManufacturerInfoRead />"
+
+
+class UserManufacturerInfoResponse(APCI):
+    """UserManufacturerInfoResponse service."""
+
+    code = APCIUserService.USER_MANUFACTURER_INFO_RESPONSE
+
+    def __init__(self, manufacturer_id: int = 0, data: Optional[bytes] = None) -> None:
+        """Initialize a new instance of UserManufacturerInfoResponse."""
+        if data is None:
+            data = bytes([0x00, 0x00])
+
+        self.manufacturer_id = manufacturer_id
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        self.manufacturer_id, self.data = struct.unpack("!B2s", raw[2:])
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        payload = struct.pack("!B2s", self.manufacturer_id, self.data)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<UserManufacturerInfoResponse manufacturer_id="{self.manufacturer_id}" data="{self.data.hex()}" />'
+
+
+class FunctionPropertyCommand(APCI):
+    """FunctionPropertyCommand service."""
+
+    code = APCIUserService.FUNCTION_PROPERTY_COMMAND
+
+    def __init__(
+        self, object_index: int = 0, property_id: int = 0, data: Optional[bytes] = None
+    ) -> None:
+        """Initialize a new instance of FunctionPropertyCommand."""
+        if data is None:
+            data = bytes()
+
+        self.object_index = object_index
+        self.property_id = property_id
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 4
+
+        self.object_index, self.property_id, self.data = struct.unpack(
+            f"!BB{size}s", raw[2:]
+        )
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        size = len(self.data)
+        payload = struct.pack(
+            f"!BB{size}s", self.object_index, self.property_id, self.data
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<FunctionPropertyCommand object_index="{self.object_index}" property_id="{self.property_id}" data="{self.data.hex()}" />'
+
+
+class FunctionPropertyStateRead(APCI):
+    """FunctionPropertyStateRead service."""
+
+    code = APCIUserService.FUNCTION_PROPERTY_STATE_READ
+
+    def __init__(
+        self, object_index: int = 0, property_id: int = 0, data: Optional[bytes] = None
+    ) -> None:
+        """Initialize a new instance of FunctionPropertyStateRead."""
+        if data is None:
+            data = bytes()
+
+        self.object_index = object_index
+        self.property_id = property_id
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 3 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 4
+
+        self.object_index, self.property_id, self.data = struct.unpack(
+            f"!BB{size}s", raw[2:]
+        )
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        size = len(self.data)
+        payload = struct.pack(
+            f"!BB{size}s", self.object_index, self.property_id, self.data
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<FunctionPropertyStateRead object_index="{self.object_index}" property_id="{self.property_id}" data="{self.data.hex()}" />'
+
+
+class FunctionPropertyStateResponse(APCI):
+    """FunctionPropertyStateResponse service."""
+
+    code = APCIUserService.FUNCTION_PROPERTY_STATE_READ
+
+    def __init__(
+        self,
+        object_index: int = 0,
+        property_id: int = 0,
+        return_code: int = 0,
+        data: Optional[bytes] = None,
+    ) -> None:
+        """Initialize a new instance of FunctionPropertyStateResponse."""
+        if data is None:
+            data = bytes()
+
+        self.object_index = object_index
+        self.property_id = property_id
+        self.return_code = return_code
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 5
+
+        (
+            self.object_index,
+            self.property_id,
+            self.return_code,
+            self.data,
+        ) = struct.unpack(f"!BBB{size}s", raw[2:])
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        size = len(self.data)
+        payload = struct.pack(
+            f"!BBB{size}s",
+            self.object_index,
+            self.property_id,
+            self.return_code,
+            self.data,
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<FunctionPropertyStateResponse object_index="{self.object_index}" property_id="{self.property_id}" return_code="{self.return_code}" data="{self.data.hex()}" />'
+
+
+class AuthorizeRequest(APCI):
+    """AuthorizeRequest service."""
+
+    code = APCIExtendedService.AUTHORIZE_REQUEST
+
+    def __init__(self, key: int = 0) -> None:
+        """Initialize a new instance of AuthorizeRequest."""
+        self.key = key
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 5
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        _, self.key = struct.unpack("!BI", raw[2:])
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        payload = struct.pack("!BI", 0, self.key)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<AuthorizeRequest key="{self.key}" />'
+
+
+class AuthorizeResponse(APCI):
+    """AuthorizeResponse service."""
+
+    code = APCIExtendedService.AUTHORIZE_RESPONSE
+
+    def __init__(self, level: int = 0) -> None:
+        """Initialize a new instance of AuthorizeResponse."""
+        self.level = level
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 2
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        (self.level,) = struct.unpack("!B", raw[2:])
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        payload = struct.pack("!B", self.level)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<AuthorizeResponse level="{self.level}"/>'
+
+
+class PropertyValueRead(APCI):
+    """
+    PropertyValueRead service.
+
+    Payload indicates object, property, count and start.
+    """
+
+    code = APCIExtendedService.PROPERTY_VALUE_READ
+
+    def __init__(
+        self,
+        object_index: int = 0,
+        property_id: int = 0,
+        count: int = 0,
+        start_index: int = 1,
+    ) -> None:
+        """Initialize a new instance of PropertyValueRead."""
+        self.object_index = object_index
+        self.property_id = property_id
+        self.count = count
+        self.start_index = start_index
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 5
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        (
+            self.object_index,
+            self.property_id,
+            count,
+            self.start_index,
+        ) = struct.unpack("!BBBB", raw[2:])
+
+        self.count = count >> 4
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.count < 0 or self.count > 2 ** 4:
+            raise ConversionError("Count out of range.")
+
+        payload = struct.pack(
+            "!BBBB",
+            self.object_index,
+            self.property_id,
+            self.count << 4,
+            self.start_index,
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return (
+            '<PropertyValueRead object_index="{}" property_id="{}" count="{}" '
+            'start_index="{}" />'.format(
+                self.object_index, self.property_id, self.count, self.start_index
+            )
+        )
+
+
+class PropertyValueWrite(APCI):
+    """
+    PropertyValueWrite service.
+
+    Payload indicates object, property, count, start and data itself.
+    """
+
+    code = APCIExtendedService.PROPERTY_VALUE_WRITE
+
+    def __init__(
+        self,
+        object_index: int = 0,
+        property_id: int = 0,
+        count: int = 0,
+        start_index: int = 0,
+        data: Optional[bytes] = None,
+    ) -> None:
+        """Initialize a new instance of PropertyValueWrite."""
+
+        if data is None:
+            data = bytearray()
+
+        self.object_index = object_index
+        self.property_id = property_id
+        self.count = count
+        self.start_index = start_index
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 5 + len(self.data)
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.count < 0 or self.count > 2 ** 4:
+            raise ConversionError("Count out of range.")
+
+        size = len(self.data)
+        count = self.count << 4
+        payload = struct.pack(
+            f"!BBBB{size}s",
+            self.object_index,
+            self.property_id,
+            count,
+            self.start_index,
+            self.data,
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 6
+
+        (
+            _,
+            self.object_index,
+            self.property_id,
+            self.count,
+            self.start_index,
+            self.data,
+        ) = struct.unpack(f"!BBBBB{size}s", raw[1:])
+
+        self.count = self.count >> 4
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return (
+            '<PropertyValueWrite object_index="{}" property_id="{}" count="{}" '
+            'start_index="{}" data="{}" />'.format(
+                self.object_index,
+                self.property_id,
+                self.count,
+                self.start_index,
+                self.data.hex(),
+            )
+        )
+
+
+class PropertyValueResponse(APCI):
+    """
+    PropertyValueResponse service.
+
+    Payload indicates object, property, count, start and data itself. Size of
+    the payload depends on the data.
+    """
+
+    code = APCIExtendedService.PROPERTY_VALUE_RESPONSE
+
+    def __init__(
+        self,
+        object_index: int = 0,
+        property_id: int = 0,
+        count: int = 0,
+        start_index: int = 0,
+        data: Optional[bytes] = None,
+    ) -> None:
+        """Initialize a new instance of PropertyValueResponse."""
+
+        if data is None:
+            data = bytearray()
+
+        self.object_index = object_index
+        self.property_id = property_id
+        self.count = count
+        self.start_index = start_index
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 5 + len(self.data)
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        size = len(raw) - 6
+
+        (
+            _,
+            self.object_index,
+            self.property_id,
+            self.count,
+            self.start_index,
+            self.data,
+        ) = struct.unpack(f"!BBBBB{size}s", raw[1:])
+
+        self.count = self.count >> 4
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.count < 0 or self.count > 2 ** 4:
+            raise ConversionError("Count out of range.")
+
+        size = len(self.data)
+        count = self.count << 4
+        payload = struct.pack(
+            f"!BBBB{size}s",
+            self.object_index,
+            self.property_id,
+            count,
+            self.start_index,
+            self.data,
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return (
+            '<PropertyValueResponse object_index="{}" property_id="{}" count="{}" '
+            'start_index="{}" data="{}" />'.format(
+                self.object_index,
+                self.property_id,
+                self.count,
+                self.start_index,
+                self.data.hex(),
+            )
+        )
+
+
+class PropertyDescriptionRead(APCI):
+    """PropertyDescriptionRead service."""
+
+    code = APCIExtendedService.PROPERTY_DESCRIPTION_READ
+
+    def __init__(
+        self, object_index: int = 0, property_id: int = 0, property_index: int = 0
+    ) -> None:
+        """Initialize a new instance of PropertyDescriptionRead."""
+        self.object_index = object_index
+        self.property_id = property_id
+        self.property_index = property_index
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 4
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        self.object_index, self.property_id, self.property_index = struct.unpack(
+            "!BBB", raw[2:]
+        )
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        payload = struct.pack(
+            "!BBB", self.object_index, self.property_id, self.property_index
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<PropertyDescriptionRead object_index="{self.object_index}" property_id="{self.property_id}" property_index="{self.property_index}" />'
+
+
+class PropertyDescriptionResponse(APCI):
+    """PropertyDescriptionResponse service."""
+
+    code = APCIExtendedService.PROPERTY_DESCRIPTION_RESPONSE
+
+    def __init__(
+        self,
+        object_index: int = 0,
+        property_id: int = 0,
+        property_index: int = 0,
+        type_: int = 0,
+        max_count: int = 0,
+        access: int = 0,
+    ) -> None:
+        """Initialize a new instance of PropertyDescriptionRead."""
+        self.object_index = object_index
+        self.property_id = property_id
+        self.property_index = property_index
+        self.type = type_
+        self.max_count = max_count
+        self.access = access
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 8
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        (
+            self.object_index,
+            self.property_id,
+            self.property_index,
+            self.type,
+            max_count,
+            self.access,
+        ) = struct.unpack("!BBBBHB", raw[2:])
+
+        self.max_count = max_count & 0x0FFF
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.max_count < 0 or self.max_count >= 2 ** 12:
+            raise ConversionError("Max count out of range.")
+
+        payload = struct.pack(
+            "!BBBBHB",
+            self.object_index,
+            self.property_id,
+            self.property_index,
+            self.type,
+            self.max_count & 0x0FFF,
+            self.access,
+        )
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<PropertyDescriptionResponse object_index="{self.object_index}" property_id="{self.property_id}" property_index="{self.property_index}" type="{self.type}" max_count="{self.max_count}" access="{self.access}" />'
+
+
+class IndividualAddressSerialRead(APCI):
+    """IndividualAddressSerialRead service."""
+
+    code = APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_READ
+
+    def __init__(self, serial: Optional[bytes] = None) -> None:
+        """Initialize a new instance of PropertyDescriptionRead."""
+        if serial is None:
+            serial = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
+
+        self.serial = serial
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 7
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        (self.serial,) = struct.unpack("!6s", raw[2:])
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if len(self.serial) != 6:
+            raise ConversionError("Serial must be 6 bytes.")
+
+        payload = struct.pack("!6s", self.serial)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<IndividualAddressSerialRead serial="{self.serial.hex()}" />'
+
+
+class IndividualAddressSerialResponse(APCI):
+    """IndividualAddressSerialResponse service."""
+
+    code = APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_RESPONSE
+
+    def __init__(
+        self,
+        serial: Optional[bytes] = None,
+        address: Optional[IndividualAddress] = None,
+    ) -> None:
+        """Initialize a new instance of IndividualAddressSerialResponse."""
+        if serial is None:
+            serial = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
+        if address is None:
+            address = IndividualAddress("0.0.0")
+
+        self.serial = serial
+        self.address = address
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 11
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        self.serial, address_high, address_low, _ = struct.unpack("!6sBBH", raw[2:])
+
+        self.address = IndividualAddress((address_high, address_low))
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if len(self.serial) != 6:
+            raise ConversionError("Serial must be 6 bytes.")
+
+        address_high, address_low = self.address.to_knx()
+        payload = struct.pack("!6sBBH", self.serial, address_high, address_low, 0)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<IndividualAddressSerialResponse serial="{self.serial.hex()}" address="{self.address}" />'
+
+
+class IndividualAddressSerialWrite(APCI):
+    """IndividualAddressSerialWrite service."""
+
+    code = APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_WRITE
+
+    def __init__(
+        self,
+        serial: Optional[bytes] = None,
+        address: Optional[IndividualAddress] = None,
+    ) -> None:
+        """Initialize a new instance of IndividualAddressSerialWrite."""
+        if serial is None:
+            serial = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
+        if address is None:
+            address = IndividualAddress("0.0.0")
+
+        self.serial = serial
+        self.address = address
+
+    def calculated_length(self) -> int:
+        """Get length of APCI payload."""
+        return 13
+
+    def from_knx(self, raw: bytes) -> None:
+        """Parse/deserialize from KNX/IP raw data."""
+        self.serial, address_high, address_low, _ = struct.unpack("!6sBBI", raw[2:])
+
+        self.address = IndividualAddress((address_high, address_low))
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if len(self.serial) != 6:
+            raise ConversionError("Serial must be 6 bytes.")
+
+        address_high, address_low = self.address.to_knx()
+        payload = struct.pack("!6sBBI", self.serial, address_high, address_low, 0)
+
+        return encode_cmd_and_payload(self.code, appended_payload=payload)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<IndividualAddressSerialWrite serial="{self.serial.hex()}" address="{self.address}" />'


Test patch:
diff --git a/test/core_tests/payload_reader_test.py b/test/core_tests/payload_reader_test.py
new file mode 100644
index 00000000..a73da03e
--- /dev/null
+++ b/test/core_tests/payload_reader_test.py
@@ -0,0 +1,83 @@
+"""Unit test for payload reader."""
+import asyncio
+import unittest
+from unittest.mock import MagicMock, patch
+
+from xknx import XKNX
+from xknx.core import PayloadReader
+from xknx.telegram import IndividualAddress, Telegram, TelegramDirection
+from xknx.telegram.apci import MemoryRead, MemoryResponse
+
+
+class TestPayloadReader(unittest.TestCase):
+    """Test class for payload reader."""
+
+    def setUp(self):
+        """Set up test class."""
+        self.loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(self.loop)
+
+    def tearDown(self):
+        """Tear down test class."""
+        self.loop.close()
+
+    def create_telegram_queue_mock(self, xknx: XKNX, response_telegram: Telegram):
+        """
+        Create a TelegramQueue mock that returns a specific response telegram.
+        """
+        xknx.telegram_queue = MagicMock()
+
+        def _register_telegram_received_cb(func):
+            self.loop.create_task(func(response_telegram))
+
+        xknx.telegram_queue.register_telegram_received_cb.side_effect = (
+            _register_telegram_received_cb
+        )
+
+    def test_payload_reader_send_success(self):
+        """Test payload reader: successful send."""
+        xknx = XKNX()
+
+        destination_address = IndividualAddress("1.2.3")
+        request_payload = MemoryRead(0xAABB, 3)
+        response_payload = MemoryResponse(0xAABB, 3, bytes([0x00, 0x11, 0x33]))
+
+        response_telegram = Telegram(
+            source_address=destination_address,
+            direction=TelegramDirection.INCOMING,
+            payload=response_payload,
+        )
+
+        self.create_telegram_queue_mock(xknx, response_telegram)
+
+        payload_reader = PayloadReader(xknx, destination_address)
+
+        payload = self.loop.run_until_complete(
+            payload_reader.send(request_payload, response_class=MemoryResponse)
+        )
+
+        # Response is received.
+        self.assertEqual(payload, response_payload)
+
+    @patch("logging.Logger.warning")
+    def test_payload_reader_send_timeout(self, logger_warning_mock):
+        """Test payload reader: timeout while waiting for response."""
+        xknx = XKNX()
+
+        destination_address = IndividualAddress("1.2.3")
+        request_payload = MemoryRead(0xAABB, 3)
+
+        payload_reader = PayloadReader(xknx, destination_address, timeout_in_seconds=0)
+
+        payload = self.loop.run_until_complete(
+            payload_reader.send(request_payload, response_class=MemoryResponse)
+        )
+
+        # No response received.
+        self.assertEqual(payload, None)
+        # Warning was logged.
+        logger_warning_mock.assert_called_once_with(
+            "Error: KNX bus did not respond in time (%s secs) to payload request for: %s",
+            0.0,
+            IndividualAddress("1.2.3"),
+        )
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
index ab7ba401..c8f85dab 100644
--- a/test/io_tests/tunnel_test.py
+++ b/test/io_tests/tunnel_test.py
@@ -48,18 +48,8 @@ class TestTunnelling(unittest.TestCase):
         send_ack_mock.assert_called_once_with(0x02, 0x21)
 
     @patch("xknx.io.Tunnel._send_tunnelling_ack")
-    def test_tunnel_request_received_unsupported_frames(self, send_ack_mock):
+    def test_tunnel_request_received_cemi_too_small(self, send_ack_mock):
         """Test Tunnel sending ACK for unsupported frames."""
-        # LDataInd APciPhysAddrRead from 0.0.1 to 0/0/0 broadcast - ETS scan for devices in programming mode
-        # <UnsupportedCEMIMessage description="APCI not supported: 0b0100000000 in CEMI: 2900b0d000010000010100" />
-        # communication_channel_id: 0x02   sequence_counter: 0x4f
-        raw = bytes.fromhex("0610 0420 0015 04 02 4f 00 2900b0d000010000010100")
-
-        self.tunnel.udp_client.data_received_callback(raw)
-        self.tg_received_mock.assert_not_called()
-        send_ack_mock.assert_called_once_with(0x02, 0x4F)
-        send_ack_mock.reset_mock()
-
         # LDataInd T_Connect from 1.0.250 to 1.0.255 (xknx tunnel endpoint) - ETS Line-Scan
         # <UnsupportedCEMIMessage description="CEMI too small. Length: 10; CEMI: 2900b06010fa10ff0080" />
         # communication_channel_id: 0x02   sequence_counter: 0x81
@@ -68,3 +58,15 @@ class TestTunnelling(unittest.TestCase):
         self.tunnel.udp_client.data_received_callback(raw)
         self.tg_received_mock.assert_not_called()
         send_ack_mock.assert_called_once_with(0x02, 0x81)
+
+    @patch("xknx.io.Tunnel._send_tunnelling_ack")
+    def test_tunnel_request_received_apci_unsupported(self, send_ack_mock):
+        """Test Tunnel sending ACK for unsupported frames."""
+        # LDataInd Unsupported Extended APCI from 0.0.1 to 0/0/0 broadcast
+        # <UnsupportedCEMIMessage description="APCI not supported: 0b1111111000 in CEMI: 2900b0d0000100000103f8" />
+        # communication_channel_id: 0x02   sequence_counter: 0x4f
+        raw = bytes.fromhex("0610 0420 0015 04 02 4f 00 2900b0d0000100000103f8")
+
+        self.tunnel.udp_client.data_received_callback(raw)
+        self.tg_received_mock.assert_not_called()
+        send_ack_mock.assert_called_once_with(0x02, 0x4F)
diff --git a/test/knxip_tests/cemi_frame_test.py b/test/knxip_tests/cemi_frame_test.py
index 84993d92..0dbdc19c 100644
--- a/test/knxip_tests/cemi_frame_test.py
+++ b/test/knxip_tests/cemi_frame_test.py
@@ -50,7 +50,7 @@ def test_valid_command(frame):
 
 
 def test_invalid_tpci_apci(frame):
-    """Test for invalid APCICommand"""
+    """Test for invalid APCIService"""
     with raises(UnsupportedCEMIMessage, match=r".*APCI not supported: .*"):
         frame.from_knx_data_link_layer(get_data(0x29, 0, 0, 0, 0, 1, 0xFFC0, []))
 
diff --git a/test/telegram_tests/apci_test.py b/test/telegram_tests/apci_test.py
index 7cb42ea8..9145e915 100644
--- a/test/telegram_tests/apci_test.py
+++ b/test/telegram_tests/apci_test.py
@@ -4,18 +4,128 @@ import unittest
 from pytest import raises
 from xknx.dpt import DPTArray, DPTBinary
 from xknx.exceptions import ConversionError
-from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite
+from xknx.telegram.address import IndividualAddress
+from xknx.telegram.apci import (
+    APCI,
+    ADCRead,
+    ADCResponse,
+    APCIExtendedService,
+    APCIService,
+    APCIUserService,
+    AuthorizeRequest,
+    AuthorizeResponse,
+    DeviceDescriptorRead,
+    DeviceDescriptorResponse,
+    FunctionPropertyCommand,
+    FunctionPropertyStateRead,
+    FunctionPropertyStateResponse,
+    GroupValueRead,
+    GroupValueResponse,
+    GroupValueWrite,
+    IndividualAddressRead,
+    IndividualAddressResponse,
+    IndividualAddressSerialRead,
+    IndividualAddressSerialResponse,
+    IndividualAddressSerialWrite,
+    IndividualAddressWrite,
+    MemoryRead,
+    MemoryResponse,
+    MemoryWrite,
+    PropertyDescriptionRead,
+    PropertyDescriptionResponse,
+    PropertyValueRead,
+    PropertyValueResponse,
+    PropertyValueWrite,
+    Restart,
+    UserManufacturerInfoRead,
+    UserManufacturerInfoResponse,
+    UserMemoryRead,
+    UserMemoryResponse,
+    UserMemoryWrite,
+)
 
 
 class TestAPCI(unittest.TestCase):
     """Test class for APCI objects."""
 
     def test_resolve_apci(self):
-        """Test resolve_apci for supported services."""
+        """Test resolve_apci for supported APCI services."""
         test_cases = [
-            (GroupValueRead.code.value, GroupValueRead),
-            (GroupValueWrite.code.value, GroupValueWrite),
-            (GroupValueResponse.code.value, GroupValueResponse),
+            (APCIService.GROUP_READ.value, GroupValueRead),
+            (APCIService.GROUP_WRITE.value, GroupValueWrite),
+            (APCIService.GROUP_RESPONSE.value, GroupValueResponse),
+            (APCIService.INDIVIDUAL_ADDRESS_WRITE.value, IndividualAddressWrite),
+            (APCIService.INDIVIDUAL_ADDRESS_READ.value, IndividualAddressRead),
+            (APCIService.INDIVIDUAL_ADDRESS_RESPONSE.value, IndividualAddressResponse),
+            (APCIService.ADC_READ.value, ADCRead),
+            (APCIService.ADC_RESPONSE.value, ADCResponse),
+            (APCIService.MEMORY_READ.value, MemoryRead),
+            (APCIService.MEMORY_WRITE.value, MemoryWrite),
+            (APCIService.MEMORY_RESPONSE.value, MemoryResponse),
+            (APCIService.DEVICE_DESCRIPTOR_READ.value, DeviceDescriptorRead),
+            (APCIService.DEVICE_DESCRIPTOR_RESPONSE.value, DeviceDescriptorResponse),
+            (APCIService.RESTART.value, Restart),
+        ]
+
+        for code, clazz in test_cases:
+            self.assertIsInstance(APCI.resolve_apci(code), clazz)
+
+    def test_resolve_class_user(self):
+        """Test resolve_class for supported user APCI services."""
+        test_cases = [
+            (APCIUserService.USER_MEMORY_READ.value, UserMemoryRead),
+            (APCIUserService.USER_MEMORY_RESPONSE.value, UserMemoryResponse),
+            (APCIUserService.USER_MEMORY_WRITE.value, UserMemoryWrite),
+            (
+                APCIUserService.USER_MANUFACTURER_INFO_READ.value,
+                UserManufacturerInfoRead,
+            ),
+            (
+                APCIUserService.USER_MANUFACTURER_INFO_RESPONSE.value,
+                UserManufacturerInfoResponse,
+            ),
+            (APCIUserService.FUNCTION_PROPERTY_COMMAND.value, FunctionPropertyCommand),
+            (
+                APCIUserService.FUNCTION_PROPERTY_STATE_READ.value,
+                FunctionPropertyStateRead,
+            ),
+            (
+                APCIUserService.FUNCTION_PROPERTY_STATE_RESPONSE.value,
+                FunctionPropertyStateResponse,
+            ),
+        ]
+
+        for code, clazz in test_cases:
+            self.assertIsInstance(APCI.resolve_apci(code), clazz)
+
+    def test_resolve_class_extended(self):
+        """Test resolve_class for supported extended APCI services."""
+        test_cases = [
+            (APCIExtendedService.AUTHORIZE_REQUEST.value, AuthorizeRequest),
+            (APCIExtendedService.AUTHORIZE_RESPONSE.value, AuthorizeResponse),
+            (APCIExtendedService.PROPERTY_VALUE_READ.value, PropertyValueRead),
+            (APCIExtendedService.PROPERTY_VALUE_WRITE.value, PropertyValueWrite),
+            (APCIExtendedService.PROPERTY_VALUE_RESPONSE.value, PropertyValueResponse),
+            (
+                APCIExtendedService.PROPERTY_DESCRIPTION_READ.value,
+                PropertyDescriptionRead,
+            ),
+            (
+                APCIExtendedService.PROPERTY_DESCRIPTION_RESPONSE.value,
+                PropertyDescriptionResponse,
+            ),
+            (
+                APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_READ.value,
+                IndividualAddressSerialRead,
+            ),
+            (
+                APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_RESPONSE.value,
+                IndividualAddressSerialResponse,
+            ),
+            (
+                APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_WRITE.value,
+                IndividualAddressSerialWrite,
+            ),
         ]
 
         for code, clazz in test_cases:
@@ -25,11 +135,11 @@ class TestAPCI(unittest.TestCase):
         """Test resolve_apci for unsupported services."""
 
         with raises(ConversionError, match=r".*Class not implemented for APCI.*"):
-            APCI.resolve_apci(0x0100)
+            # Unsupported user service.
+            APCI.resolve_apci(0x02C3)
 
-        with raises(
-            ConversionError, match=r".*Class not implemented for extended APCI.*"
-        ):
+        with raises(ConversionError, match=r".*Class not implemented for APCI.*"):
+            # Unsupported extended service.
             APCI.resolve_apci(0x03C0)
 
 
@@ -118,7 +228,7 @@ class TestGroupValueWrite(unittest.TestCase):
 
 
 class TestGroupValueResponse(unittest.TestCase):
-    """Test class for TestGroupValueResponse objects."""
+    """Test class for GroupValueResponse objects."""
 
     def test_calculated_length(self):
         """Test the test_calculated_length method."""
@@ -170,3 +280,1263 @@ class TestGroupValueResponse(unittest.TestCase):
         self.assertEqual(
             str(payload), '<GroupValueResponse value="<DPTBinary value="1" />" />'
         )
+
+
+class TestIndividualAddressWrite(unittest.TestCase):
+    """Test class for IndividualAddressWrite objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = IndividualAddressWrite()
+
+        self.assertEqual(payload.calculated_length(), 3)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = IndividualAddressWrite()
+        payload.from_knx(bytes([0x00, 0xC0, 0x12, 0x03]))
+
+        self.assertEqual(payload, IndividualAddressWrite(IndividualAddress("1.2.3")))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = IndividualAddressWrite(IndividualAddress("1.2.3"))
+
+        self.assertEqual(payload.to_knx(), bytes([0x00, 0xC0, 0x12, 0x03]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = IndividualAddressWrite(IndividualAddress("1.2.3"))
+
+        self.assertEqual(str(payload), '<IndividualAddressWrite address="1.2.3" />')
+
+
+class TestIndividualAddressRead(unittest.TestCase):
+    """Test class for IndividualAddressRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = IndividualAddressRead()
+
+        self.assertEqual(payload.calculated_length(), 1)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = IndividualAddressRead()
+        payload.from_knx(bytes([0x01, 0x00]))
+
+        self.assertEqual(payload, IndividualAddressRead())
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = IndividualAddressRead()
+
+        self.assertEqual(payload.to_knx(), bytes([0x01, 0x00]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = IndividualAddressRead()
+
+        self.assertEqual(str(payload), "<IndividualAddressRead />")
+
+
+class TestIndividualAddressResponse(unittest.TestCase):
+    """Test class for IndividualAddressResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = IndividualAddressRead()
+
+        self.assertEqual(payload.calculated_length(), 1)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = IndividualAddressResponse()
+        payload.from_knx(bytes([0x01, 0x40]))
+
+        self.assertEqual(payload, IndividualAddressResponse())
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = IndividualAddressResponse()
+
+        self.assertEqual(payload.to_knx(), bytes([0x01, 0x40]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = IndividualAddressResponse()
+
+        self.assertEqual(str(payload), "<IndividualAddressResponse />")
+
+
+class TestADCRead(unittest.TestCase):
+    """Test class for ADCRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = ADCRead()
+
+        self.assertEqual(payload.calculated_length(), 2)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = ADCRead()
+        payload.from_knx(bytes([0x01, 0x82, 0x04]))
+
+        self.assertEqual(payload, ADCRead(channel=2, count=4))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = ADCRead(channel=1, count=3)
+
+        self.assertEqual(payload.to_knx(), bytes([0x01, 0x81, 0x03]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = ADCRead(channel=1, count=3)
+
+        self.assertEqual(str(payload), '<ADCRead channel="1" count="3" />')
+
+
+class TestADCResponse(unittest.TestCase):
+    """Test class for ADCResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = ADCResponse()
+
+        self.assertEqual(payload.calculated_length(), 4)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = ADCResponse()
+        payload.from_knx(bytes([0x01, 0xC2, 0x04, 0x03, 0xFF]))
+
+        self.assertEqual(payload, ADCResponse(channel=2, count=4, value=1023))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = ADCResponse(channel=1, count=3, value=456)
+
+        self.assertEqual(payload.to_knx(), bytes([0x01, 0xC1, 0x03, 0x01, 0xC8]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = ADCResponse(channel=1, count=3, value=456)
+
+        self.assertEqual(
+            str(payload), '<ADCResponse channel="1" count="3" value="456" />'
+        )
+
+
+class TestMemoryRead(unittest.TestCase):
+    """Test class for MemoryRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = MemoryRead()
+
+        self.assertEqual(payload.calculated_length(), 3)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = MemoryRead()
+        payload.from_knx(bytes([0x02, 0x0B, 0x12, 0x34]))
+
+        self.assertEqual(payload, MemoryRead(address=0x1234, count=11))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = MemoryRead(address=0x1234, count=11)
+
+        self.assertEqual(payload.to_knx(), bytes([0x02, 0x0B, 0x12, 0x34]))
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = MemoryRead(address=0xAABBCCDD, count=11)
+
+        with self.assertRaisesRegex(ConversionError, r".*Address.*"):
+            payload.to_knx()
+
+        payload = MemoryRead(address=0x1234, count=255)
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = MemoryRead(address=0x1234, count=11)
+
+        self.assertEqual(str(payload), '<MemoryRead address="0x1234" count="11" />')
+
+
+class TestMemoryWrite(unittest.TestCase):
+    """Test class for MemoryWrite objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = MemoryWrite(address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(payload.calculated_length(), 6)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = MemoryWrite()
+        payload.from_knx(bytes([0x02, 0x83, 0x12, 0x34, 0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(
+            payload,
+            MemoryWrite(address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = MemoryWrite(address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x02, 0x83, 0x12, 0x34, 0xAA, 0xBB, 0xCC])
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = MemoryWrite(
+            address=0xAABBCCDD, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Address.*"):
+            payload.to_knx()
+
+        payload = MemoryWrite(address=0x1234, count=255, data=bytes([0xAA, 0xBB, 0xCC]))
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = MemoryWrite(address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(
+            str(payload), '<MemoryWrite address="0x1234" count="3" data="aabbcc" />'
+        )
+
+
+class TestMemoryResponse(unittest.TestCase):
+    """Test class for MemoryResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = MemoryResponse(
+            address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(payload.calculated_length(), 6)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = MemoryResponse()
+        payload.from_knx(bytes([0x02, 0x43, 0x12, 0x34, 0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(
+            payload,
+            MemoryResponse(address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = MemoryResponse(
+            address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x02, 0x43, 0x12, 0x34, 0xAA, 0xBB, 0xCC])
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = MemoryResponse(
+            address=0xAABBCCDD, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Address.*"):
+            payload.to_knx()
+
+        payload = MemoryResponse(
+            address=0x1234, count=255, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = MemoryResponse(
+            address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(
+            str(payload), '<MemoryResponse address="0x1234" count="3" data="aabbcc" />'
+        )
+
+
+class TestDeviceDescriptorRead(unittest.TestCase):
+    """Test class for DeviceDescriptorRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = DeviceDescriptorRead(0)
+
+        self.assertEqual(payload.calculated_length(), 1)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = DeviceDescriptorRead()
+        payload.from_knx(bytes([0x03, 0x0D]))
+
+        self.assertEqual(payload, DeviceDescriptorRead(13))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = DeviceDescriptorRead(13)
+
+        self.assertEqual(payload.to_knx(), bytes([0x03, 0x0D]))
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = DeviceDescriptorRead(255)
+
+        with self.assertRaisesRegex(ConversionError, r".*Descriptor.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = DeviceDescriptorRead(0)
+
+        self.assertEqual(str(payload), '<DeviceDescriptorRead descriptor="0" />')
+
+
+class TestDeviceDescriptorResponse(unittest.TestCase):
+    """Test class for DeviceDescriptorResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = DeviceDescriptorResponse(descriptor=0, value=123)
+
+        self.assertEqual(payload.calculated_length(), 3)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = DeviceDescriptorResponse()
+        payload.from_knx(bytes([0x03, 0x4D, 0x00, 0x7B]))
+
+        self.assertEqual(payload, DeviceDescriptorResponse(descriptor=13, value=123))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = DeviceDescriptorResponse(descriptor=13, value=123)
+
+        self.assertEqual(payload.to_knx(), bytes([0x03, 0x4D, 0x00, 0x7B]))
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = DeviceDescriptorRead(255)
+
+        with self.assertRaisesRegex(ConversionError, r".*Descriptor.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = DeviceDescriptorResponse(descriptor=0, value=123)
+
+        self.assertEqual(
+            str(payload), '<DeviceDescriptorResponse descriptor="0" value="123" />'
+        )
+
+
+class TestUserMemoryRead(unittest.TestCase):
+    """Test class for UserMemoryRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = UserMemoryRead()
+
+        self.assertEqual(payload.calculated_length(), 4)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = UserMemoryRead()
+        payload.from_knx(bytes([0x02, 0xC0, 0x1B, 0x23, 0x45]))
+
+        self.assertEqual(payload, UserMemoryRead(address=0x12345, count=11))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = UserMemoryRead(address=0x12345, count=11)
+
+        self.assertEqual(payload.to_knx(), bytes([0x02, 0xC0, 0x1B, 0x23, 0x45]))
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = UserMemoryRead(address=0xAABBCCDD, count=11)
+
+        with self.assertRaisesRegex(ConversionError, r".*Address.*"):
+            payload.to_knx()
+
+        payload = UserMemoryRead(address=0x12345, count=255)
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = UserMemoryRead(address=0x12345, count=11)
+
+        self.assertEqual(
+            str(payload), '<UserMemoryRead address="0x12345" count="11" />'
+        )
+
+
+class TestUserMemoryWrite(unittest.TestCase):
+    """Test class for UserMemoryWrite objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = UserMemoryWrite(
+            address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(payload.calculated_length(), 7)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = UserMemoryWrite()
+        payload.from_knx(bytes([0x02, 0xC2, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(
+            payload,
+            UserMemoryWrite(address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = UserMemoryWrite(
+            address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x02, 0xC2, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC])
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = UserMemoryWrite(
+            address=0xAABBCCDD, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Address.*"):
+            payload.to_knx()
+
+        payload = UserMemoryWrite(
+            address=0x12345, count=255, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = UserMemoryWrite(
+            address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<UserMemoryWrite address="0x12345" count="3" data="aabbcc" />',
+        )
+
+
+class TestUserMemoryResponse(unittest.TestCase):
+    """Test class for UserMemoryResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = UserMemoryResponse(
+            address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(payload.calculated_length(), 7)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = UserMemoryResponse()
+        payload.from_knx(bytes([0x02, 0xC1, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC]))
+
+        self.assertEqual(
+            payload,
+            UserMemoryResponse(
+                address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = UserMemoryResponse(
+            address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x02, 0xC1, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC])
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = UserMemoryResponse(
+            address=0xAABBCCDD, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Address.*"):
+            payload.to_knx()
+
+        payload = UserMemoryResponse(
+            address=0x12345, count=255, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = UserMemoryResponse(
+            address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<UserMemoryResponse address="0x12345" count="3" data="aabbcc" />',
+        )
+
+
+class TestRestart(unittest.TestCase):
+    """Test class for Restart objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = Restart()
+
+        self.assertEqual(payload.calculated_length(), 1)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = Restart()
+        payload.from_knx(bytes([0x03, 0x80]))
+
+        self.assertEqual(payload, Restart())
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = Restart()
+
+        self.assertEqual(payload.to_knx(), bytes([0x03, 0x80]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = Restart()
+
+        self.assertEqual(str(payload), "<Restart />")
+
+
+class TestUserManufacturerInfoRead(unittest.TestCase):
+    """Test class for UserManufacturerInfoRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = UserManufacturerInfoRead()
+
+        self.assertEqual(payload.calculated_length(), 1)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = UserManufacturerInfoRead()
+        payload.from_knx(bytes([0x02, 0xC5]))
+
+        self.assertEqual(payload, UserManufacturerInfoRead())
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = UserManufacturerInfoRead()
+
+        self.assertEqual(payload.to_knx(), bytes([0x02, 0xC5]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = UserManufacturerInfoRead()
+
+        self.assertEqual(str(payload), "<UserManufacturerInfoRead />")
+
+
+class TestUserManufacturerInfoResponse(unittest.TestCase):
+    """Test class for UserManufacturerInfoResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = UserManufacturerInfoResponse(manufacturer_id=123, data=b"\x12\x34")
+
+        self.assertEqual(payload.calculated_length(), 4)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = UserManufacturerInfoResponse()
+        payload.from_knx(bytes([0x02, 0xC6, 0x7B, 0x12, 0x34]))
+
+        self.assertEqual(
+            payload, UserManufacturerInfoResponse(manufacturer_id=123, data=b"\x12\x34")
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = UserManufacturerInfoResponse(manufacturer_id=123, data=b"\x12\x34")
+
+        self.assertEqual(payload.to_knx(), bytes([0x02, 0xC6, 0x7B, 0x12, 0x34]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = UserManufacturerInfoResponse(manufacturer_id=123, data=b"\x12\x34")
+
+        self.assertEqual(
+            str(payload),
+            '<UserManufacturerInfoResponse manufacturer_id="123" data="1234" />',
+        )
+
+
+class TestFunctionPropertyCommand(unittest.TestCase):
+    """Test class for FunctionPropertyCommand objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = FunctionPropertyCommand(
+            object_index=1, property_id=4, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.calculated_length(), 5)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = FunctionPropertyCommand()
+        payload.from_knx(bytes([0x02, 0xC7, 0x01, 0x04, 0x12, 0x34]))
+
+        self.assertEqual(
+            payload,
+            FunctionPropertyCommand(object_index=1, property_id=4, data=b"\x12\x34"),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = FunctionPropertyCommand(
+            object_index=1, property_id=4, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.to_knx(), bytes([0x02, 0xC7, 0x01, 0x04, 0x12, 0x34]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = FunctionPropertyCommand(
+            object_index=1, property_id=4, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<FunctionPropertyCommand object_index="1" property_id="4" data="1234" />',
+        )
+
+
+class TestFunctionPropertyStateRead(unittest.TestCase):
+    """Test class for FunctionPropertyStateRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = FunctionPropertyStateRead(
+            object_index=1, property_id=4, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.calculated_length(), 5)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = FunctionPropertyStateRead()
+        payload.from_knx(bytes([0x02, 0xC8, 0x01, 0x04, 0x12, 0x34]))
+
+        self.assertEqual(
+            payload,
+            FunctionPropertyStateRead(object_index=1, property_id=4, data=b"\x12\x34"),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = FunctionPropertyStateRead(
+            object_index=1, property_id=4, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.to_knx(), bytes([0x02, 0xC8, 0x01, 0x04, 0x12, 0x34]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = FunctionPropertyStateRead(
+            object_index=1, property_id=4, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<FunctionPropertyStateRead object_index="1" property_id="4" data="1234" />',
+        )
+
+
+class TestFunctionPropertyStateResponse(unittest.TestCase):
+    """Test class for FunctionPropertyStateResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = FunctionPropertyStateResponse(
+            object_index=1, property_id=4, return_code=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.calculated_length(), 6)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = FunctionPropertyStateResponse()
+        payload.from_knx(bytes([0x02, 0xC8, 0x01, 0x04, 0x08, 0x12, 0x34]))
+
+        self.assertEqual(
+            payload,
+            FunctionPropertyStateResponse(
+                object_index=1, property_id=4, return_code=8, data=b"\x12\x34"
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = FunctionPropertyStateResponse(
+            object_index=1, property_id=4, return_code=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x02, 0xC8, 0x01, 0x04, 0x08, 0x12, 0x34])
+        )
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = FunctionPropertyStateResponse(
+            object_index=1, property_id=4, return_code=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<FunctionPropertyStateResponse object_index="1" property_id="4" return_code="8" data="1234" />',
+        )
+
+
+class TestAuthorizeRequest(unittest.TestCase):
+    """Test class for AuthorizeRequest objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = AuthorizeRequest(key=12345678)
+
+        self.assertEqual(payload.calculated_length(), 5)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = AuthorizeRequest()
+        payload.from_knx(bytes([0x03, 0xD1, 0x00, 0x00, 0xBC, 0x61, 0x4E]))
+
+        self.assertEqual(payload, AuthorizeRequest(key=12345678))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = AuthorizeRequest(key=12345678)
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x03, 0xD1, 0x00, 0x00, 0xBC, 0x61, 0x4E])
+        )
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = AuthorizeRequest(key=12345678)
+
+        self.assertEqual(str(payload), '<AuthorizeRequest key="12345678" />')
+
+
+class TestAuthorizeResponse(unittest.TestCase):
+    """Test class for AuthorizeResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = AuthorizeResponse(level=123)
+
+        self.assertEqual(payload.calculated_length(), 2)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = AuthorizeResponse()
+        payload.from_knx(bytes([0x03, 0xD2, 0x7B]))
+
+        self.assertEqual(payload, AuthorizeResponse(level=123))
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = AuthorizeResponse(level=123)
+
+        self.assertEqual(payload.to_knx(), bytes([0x03, 0xD2, 0x7B]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = AuthorizeResponse(level=123)
+
+        self.assertEqual(str(payload), '<AuthorizeResponse level="123"/>')
+
+
+class TestPropertyValueRead(unittest.TestCase):
+    """Test class for PropertyValueRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = PropertyValueRead(
+            object_index=1, property_id=4, count=2, start_index=8
+        )
+
+        self.assertEqual(payload.calculated_length(), 5)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = PropertyValueRead()
+        payload.from_knx(bytes([0x03, 0xD5, 0x01, 0x04, 0x20, 0x08]))
+
+        self.assertEqual(
+            payload,
+            PropertyValueRead(object_index=1, property_id=4, count=2, start_index=8),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = PropertyValueRead(
+            object_index=1, property_id=4, count=2, start_index=8
+        )
+
+        self.assertEqual(payload.to_knx(), bytes([0x03, 0xD5, 0x01, 0x04, 0x20, 0x08]))
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = PropertyValueRead(
+            object_index=1, property_id=4, count=32, start_index=8
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = PropertyValueRead(
+            object_index=1, property_id=4, count=2, start_index=8
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<PropertyValueRead object_index="1" property_id="4" count="2" start_index="8" />',
+        )
+
+
+class TestPropertyValueWrite(unittest.TestCase):
+    """Test class for PropertyValueWrite objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = PropertyValueWrite(
+            object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.calculated_length(), 7)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = PropertyValueWrite()
+        payload.from_knx(bytes([0x03, 0xD7, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34]))
+
+        self.assertEqual(
+            payload,
+            PropertyValueWrite(
+                object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = PropertyValueWrite(
+            object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x03, 0xD7, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34])
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = PropertyValueWrite(
+            object_index=1, property_id=4, count=32, start_index=8, data=b"\x12\x34"
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = PropertyValueWrite(
+            object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<PropertyValueWrite object_index="1" property_id="4" count="2" start_index="8" data="1234" />',
+        )
+
+
+class TestPropertyValueResponse(unittest.TestCase):
+    """Test class for PropertyValueResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = PropertyValueResponse(
+            object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(payload.calculated_length(), 7)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = PropertyValueResponse()
+        payload.from_knx(bytes([0x03, 0xD6, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34]))
+
+        self.assertEqual(
+            payload,
+            PropertyValueResponse(
+                object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = PropertyValueResponse(
+            object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x03, 0xD6, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34])
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = PropertyValueResponse(
+            object_index=1, property_id=4, count=32, start_index=8, data=b"\x12\x34"
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = PropertyValueResponse(
+            object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<PropertyValueResponse object_index="1" property_id="4" count="2" start_index="8" data="1234" />',
+        )
+
+
+class TestPropertyDescriptionRead(unittest.TestCase):
+    """Test class for PropertyDescriptionRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = PropertyDescriptionRead(
+            object_index=1, property_id=4, property_index=8
+        )
+
+        self.assertEqual(payload.calculated_length(), 4)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = PropertyDescriptionRead()
+        payload.from_knx(bytes([0x03, 0xD8, 0x01, 0x04, 0x08]))
+
+        self.assertEqual(
+            payload,
+            PropertyDescriptionRead(object_index=1, property_id=4, property_index=8),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = PropertyDescriptionRead(
+            object_index=1, property_id=4, property_index=8
+        )
+
+        self.assertEqual(payload.to_knx(), bytes([0x03, 0xD8, 0x01, 0x04, 0x08]))
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = PropertyDescriptionRead(
+            object_index=1, property_id=4, property_index=8
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<PropertyDescriptionRead object_index="1" property_id="4" property_index="8" />',
+        )
+
+
+class TestPropertyDescriptionResponse(unittest.TestCase):
+    """Test class for PropertyDescriptionResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = PropertyDescriptionResponse(
+            object_index=1,
+            property_id=4,
+            property_index=8,
+            type_=3,
+            max_count=5,
+            access=7,
+        )
+
+        self.assertEqual(payload.calculated_length(), 8)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = PropertyDescriptionResponse()
+        payload.from_knx(bytes([0x03, 0xD9, 0x01, 0x04, 0x08, 0x03, 0x00, 0x05, 0x07]))
+
+        self.assertEqual(
+            payload,
+            PropertyDescriptionResponse(
+                object_index=1,
+                property_id=4,
+                property_index=8,
+                type_=3,
+                max_count=5,
+                access=7,
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = PropertyDescriptionResponse(
+            object_index=1,
+            property_id=4,
+            property_index=8,
+            type_=3,
+            max_count=5,
+            access=7,
+        )
+
+        self.assertEqual(
+            payload.to_knx(),
+            bytes([0x03, 0xD9, 0x01, 0x04, 0x08, 0x03, 0x00, 0x05, 0x07]),
+        )
+
+    def test_to_knx_conversion_error(self):
+        """Test the to_knx method for conversion errors."""
+        payload = PropertyDescriptionResponse(
+            object_index=1,
+            property_id=4,
+            property_index=8,
+            type_=3,
+            max_count=4096,
+            access=7,
+        )
+
+        with self.assertRaisesRegex(ConversionError, r".*Max count.*"):
+            payload.to_knx()
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = PropertyDescriptionResponse(
+            object_index=1,
+            property_id=4,
+            property_index=8,
+            type_=3,
+            max_count=5,
+            access=7,
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<PropertyDescriptionResponse object_index="1" property_id="4" property_index="8" type="3" max_count="5" access="7" />',
+        )
+
+
+class TestIndividualAddressSerialRead(unittest.TestCase):
+    """Test class for IndividualAddressSerialRead objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = IndividualAddressSerialRead(b"\xaa\xbb\xcc\x11\x22\x33")
+
+        self.assertEqual(payload.calculated_length(), 7)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = IndividualAddressSerialRead()
+        payload.from_knx(bytes([0x03, 0xDC, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33]))
+
+        self.assertEqual(
+            payload, IndividualAddressSerialRead(b"\xaa\xbb\xcc\x11\x22\x33")
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = IndividualAddressSerialRead(b"\xaa\xbb\xcc\x11\x22\x33")
+
+        self.assertEqual(
+            payload.to_knx(), bytes([0x03, 0xDC, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33])
+        )
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = IndividualAddressSerialRead(b"\xaa\xbb\xcc\x11\x22\x33")
+
+        self.assertEqual(
+            str(payload), '<IndividualAddressSerialRead serial="aabbcc112233" />'
+        )
+
+
+class TestIndividualAddressSerialResponse(unittest.TestCase):
+    """Test class for IndividualAddressSerialResponse objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = IndividualAddressSerialResponse(
+            serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+        )
+
+        self.assertEqual(payload.calculated_length(), 11)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = IndividualAddressSerialResponse()
+        payload.from_knx(
+            bytes(
+                [0x03, 0xDD, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33, 0x12, 0x03, 0x00, 0x00]
+            )
+        )
+
+        self.assertEqual(
+            payload,
+            IndividualAddressSerialResponse(
+                serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = IndividualAddressSerialResponse(
+            serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+        )
+
+        self.assertEqual(
+            payload.to_knx(),
+            bytes(
+                [0x03, 0xDD, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33, 0x12, 0x03, 0x00, 0x00]
+            ),
+        )
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = IndividualAddressSerialResponse(
+            serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<IndividualAddressSerialResponse serial="aabbcc112233" address="1.2.3" />',
+        )
+
+
+class TestIndividualAddressSerialWrite(unittest.TestCase):
+    """Test class for IndividualAddressSerialWrite objects."""
+
+    def test_calculated_length(self):
+        """Test the test_calculated_length method."""
+        payload = IndividualAddressSerialWrite(
+            serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+        )
+
+        self.assertEqual(payload.calculated_length(), 13)
+
+    def test_from_knx(self):
+        """Test the from_knx method."""
+        payload = IndividualAddressSerialWrite()
+        payload.from_knx(
+            bytes(
+                [
+                    0x03,
+                    0xDE,
+                    0xAA,
+                    0xBB,
+                    0xCC,
+                    0x11,
+                    0x22,
+                    0x33,
+                    0x12,
+                    0x03,
+                    0x00,
+                    0x00,
+                    0x00,
+                    0x00,
+                ]
+            )
+        )
+
+        self.assertEqual(
+            payload,
+            IndividualAddressSerialWrite(
+                serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+            ),
+        )
+
+    def test_to_knx(self):
+        """Test the to_knx method."""
+        payload = IndividualAddressSerialWrite(
+            serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+        )
+
+        self.assertEqual(
+            payload.to_knx(),
+            bytes(
+                [
+                    0x03,
+                    0xDE,
+                    0xAA,
+                    0xBB,
+                    0xCC,
+                    0x11,
+                    0x22,
+                    0x33,
+                    0x12,
+                    0x03,
+                    0x00,
+                    0x00,
+                    0x00,
+                    0x00,
+                ]
+            ),
+        )
+
+    def test_str(self):
+        """Test the __str__ method."""
+        payload = IndividualAddressSerialWrite(
+            serial=b"\xaa\xbb\xcc\x11\x22\x33", address=IndividualAddress("1.2.3")
+        )
+
+        self.assertEqual(
+            str(payload),
+            '<IndividualAddressSerialWrite serial="aabbcc112233" address="1.2.3" />',
+        )