Repository: XKNX/xknx
Pull request #1154 (2023-01-22 11:22:49)

Refactor APCI to return instance of Subclass from `APCI.from_knx()`
- remove APCI.resolve_apci
- remove some default arguments to avoid unnecessary Optionals
- fix some typos

<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->


Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] 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:

- [ ] The documentation has been adjusted accordingly
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog (docs/changelog.md)

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1154](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (6e98908) into [main](https://codecov.io/gh/XKNX/xknx/commit/e031588b42c87587df7c05e5a1d547484d84433b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (e031588) will **decrease** coverage by `0.11%`.
> The diff coverage is `96.04%`.

> :exclamation: Current head 6e98908 differs from pull request most recent head 699f83a. Consider uploading reports for the commit 699f83a to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1154/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@            Coverage Diff             @@
##             main    #1154      +/-   ##
==========================================
- Coverage   96.35%   96.25%   -0.11%     
==========================================
  Files         141      141              
  Lines        8707     8709       +2     
==========================================
- Hits         8390     8383       -7     
- Misses        317      326       +9     
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/tools/group\_communication.py](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90b29scy9ncm91cF9jb21tdW5pY2F0aW9uLnB5) | `98.00% <ø> (-0.04%)` | :arrow_down: |
| [xknx/telegram/apci.py](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90ZWxlZ3JhbS9hcGNpLnB5) | `98.04% <96.00%> (-1.26%)` | :arrow_down: |
| [xknx/cemi/cemi\_frame.py](https://codecov.io/gh/XKNX/xknx/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jZW1pL2NlbWlfZnJhbWUucHk=) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index c13f35ca..738e0bee 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -12,6 +12,7 @@ nav_order: 2
 
 - Return `bytes` from `BaseAddress.to_knx()` instead of `tuple[int, int]`. This is used in `IndividualAddress` and `GroupAddress`.
 - Add `BaseAddress.from_knx()` to instantiate from `bytes`, remove instantiation form `tuple[int, int]`.
+- Refactor APCI to return complete Subclass `APCI.from_knx()` and removed `APCI.resolve_apci()`.
 
 # 2.3.0 Routing security, DPTs and CEMI-Refactoring 2023-01-10
 
diff --git a/xknx/cemi/cemi_frame.py b/xknx/cemi/cemi_frame.py
index fb5a317e..2f6795f4 100644
--- a/xknx/cemi/cemi_frame.py
+++ b/xknx/cemi/cemi_frame.py
@@ -187,12 +187,10 @@ class CEMIFrame:
                 payload=None,
             )
 
-        _apci = _apdu[0] * 256 + _apdu[1]
         try:
-            payload = APCI.resolve_apci(_apci)
+            payload = APCI.from_knx(_apdu)
         except ConversionError as err:
-            raise UnsupportedCEMIMessage(f"APCI not supported: {_apci:#012b}") from err
-        payload.from_knx(_apdu)
+            raise UnsupportedCEMIMessage("APDU not supported") from err
 
         return CEMIFrame(
             code=code,
diff --git a/xknx/telegram/apci.py b/xknx/telegram/apci.py
index f60a1ae3..e18b89ad 100644
--- a/xknx/telegram/apci.py
+++ b/xknx/telegram/apci.py
@@ -111,95 +111,100 @@ class APCI(ABC):
     def calculated_length(self) -> int:
         """Get length of APCI payload - to be implemented in derived class."""
 
-    @abstractmethod
-    def from_knx(self, raw: bytes) -> None:
-        """Parse/deserialize from KNX/IP raw data - to be implemented in derived class."""
-
     @abstractmethod
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data - to be implemented in derived class."""
+        # shall return bytearray instead of bytes so TPCI can be
+        # added to first 6 bits of first byte later
 
     def __eq__(self, other: object) -> bool:
         """Equal operator."""
+        if self.__class__ != other.__class__:
+            return False
         return self.__dict__ == other.__dict__
 
-    @staticmethod
-    def resolve_apci(apci: int) -> APCI:
+    @classmethod
+    @abstractmethod
+    def from_knx(cls, raw: bytes) -> APCI:
         """
-        Return APCI instance from APCI command.
+        Parse/deserialize from KNX/IP raw data - to be implemented in derived class.
+
+        `raw` shall be a complete APDU.
+        Return APCI instance based on APCI service.
 
         There are only 16 possible APCI services. The
         `APCIService.USER_MESSAGE` and `APCIService.ESCAPE` service have
         several sub-services.
         """
+        apci = (raw[0] * 256 + raw[1]) & 0x03FF
         service = apci & 0x03C0
 
         if service == APCIService.GROUP_READ.value:
-            return GroupValueRead()
+            return GroupValueRead.from_knx(raw)
         if service == APCIService.GROUP_WRITE.value:
-            return GroupValueWrite()
+            return GroupValueWrite.from_knx(raw)
         if service == APCIService.GROUP_RESPONSE.value:
-            return GroupValueResponse()
+            return GroupValueResponse.from_knx(raw)
         if service == APCIService.INDIVIDUAL_ADDRESS_WRITE.value:
-            return IndividualAddressWrite()
+            return IndividualAddressWrite.from_knx(raw)
         if service == APCIService.INDIVIDUAL_ADDRESS_READ.value:
-            return IndividualAddressRead()
+            return IndividualAddressRead.from_knx(raw)
         if service == APCIService.INDIVIDUAL_ADDRESS_RESPONSE.value:
-            return IndividualAddressResponse()
+            return IndividualAddressResponse.from_knx(raw)
         if service == APCIService.ADC_READ.value:
-            return ADCRead()
+            return ADCRead.from_knx(raw)
         if service == APCIService.ADC_RESPONSE.value:
-            return ADCResponse()
+            return ADCResponse.from_knx(raw)
         if service == APCIService.MEMORY_READ.value:
-            return MemoryRead()
+            return MemoryRead.from_knx(raw)
         if service == APCIService.MEMORY_WRITE.value:
-            return MemoryWrite()
+            return MemoryWrite.from_knx(raw)
         if service == APCIService.MEMORY_RESPONSE.value:
-            return MemoryResponse()
+            return MemoryResponse.from_knx(raw)
         if service == APCIService.USER_MESSAGE.value:
             if apci == APCIUserService.USER_MEMORY_READ.value:
-                return UserMemoryRead()
+                return UserMemoryRead.from_knx(raw)
             if apci == APCIUserService.USER_MEMORY_RESPONSE.value:
-                return UserMemoryResponse()
+                return UserMemoryResponse.from_knx(raw)
             if apci == APCIUserService.USER_MEMORY_WRITE.value:
-                return UserMemoryWrite()
+                return UserMemoryWrite.from_knx(raw)
             if apci == APCIUserService.USER_MANUFACTURER_INFO_READ.value:
-                return UserManufacturerInfoRead()
+                return UserManufacturerInfoRead.from_knx(raw)
             if apci == APCIUserService.USER_MANUFACTURER_INFO_RESPONSE.value:
-                return UserManufacturerInfoResponse()
+                return UserManufacturerInfoResponse.from_knx(raw)
             if apci == APCIUserService.FUNCTION_PROPERTY_COMMAND.value:
-                return FunctionPropertyCommand()
+                return FunctionPropertyCommand.from_knx(raw)
             if apci == APCIUserService.FUNCTION_PROPERTY_STATE_READ.value:
-                return FunctionPropertyStateRead()
+                return FunctionPropertyStateRead.from_knx(raw)
             if apci == APCIUserService.FUNCTION_PROPERTY_STATE_RESPONSE.value:
-                return FunctionPropertyStateResponse()
+                return FunctionPropertyStateResponse.from_knx(raw)
         if service == APCIService.DEVICE_DESCRIPTOR_READ.value:
-            return DeviceDescriptorRead()
+            return DeviceDescriptorRead.from_knx(raw)
         if service == APCIService.DEVICE_DESCRIPTOR_RESPONSE.value:
-            return DeviceDescriptorResponse()
+            return DeviceDescriptorResponse.from_knx(raw)
         if service == APCIService.RESTART.value:
-            return Restart()
+            return Restart.from_knx(raw)
         if service == APCIService.ESCAPE.value:
             if apci == APCIExtendedService.AUTHORIZE_REQUEST.value:
-                return AuthorizeRequest()
+                return AuthorizeRequest.from_knx(raw)
             if apci == APCIExtendedService.AUTHORIZE_RESPONSE.value:
-                return AuthorizeResponse()
+                return AuthorizeResponse.from_knx(raw)
             if apci == APCIExtendedService.PROPERTY_VALUE_READ.value:
-                return PropertyValueRead()
+                return PropertyValueRead.from_knx(raw)
             if apci == APCIExtendedService.PROPERTY_VALUE_WRITE.value:
-                return PropertyValueWrite()
+                return PropertyValueWrite.from_knx(raw)
             if apci == APCIExtendedService.PROPERTY_VALUE_RESPONSE.value:
-                return PropertyValueResponse()
+                return PropertyValueResponse.from_knx(raw)
             if apci == APCIExtendedService.PROPERTY_DESCRIPTION_READ.value:
-                return PropertyDescriptionRead()
+                return PropertyDescriptionRead.from_knx(raw)
             if apci == APCIExtendedService.PROPERTY_DESCRIPTION_RESPONSE.value:
-                return PropertyDescriptionResponse()
+                return PropertyDescriptionResponse.from_knx(raw)
             if apci == APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_READ.value:
-                return IndividualAddressSerialRead()
+                return IndividualAddressSerialRead.from_knx(raw)
             if apci == APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_RESPONSE.value:
-                return IndividualAddressSerialResponse()
+                return IndividualAddressSerialResponse.from_knx(raw)
             if apci == APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_WRITE.value:
-                return IndividualAddressSerialWrite()
+                return IndividualAddressSerialWrite.from_knx(raw)
 
         raise ConversionError(f"Class not implemented for APCI {apci:#012b}.")
 
@@ -217,11 +222,11 @@ class GroupValueRead(APCI):
         """Get length of APCI payload."""
         return 1
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> GroupValueRead:
         """Parse/deserialize from KNX/IP raw data."""
-
         # Nothing to parse, but must be implemented explicitly.
-        return
+        return cls()
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -241,7 +246,7 @@ class GroupValueWrite(APCI):
 
     CODE = APCIService.GROUP_WRITE
 
-    def __init__(self, value: DPTBinary | DPTArray | None = None) -> None:
+    def __init__(self, value: DPTBinary | DPTArray) -> None:
         """Initialize a new instance of GroupValueWrite."""
         self.value = value
 
@@ -249,26 +254,24 @@ class GroupValueWrite(APCI):
         """Get length of APCI payload."""
         if isinstance(self.value, DPTBinary):
             return 1
-        if isinstance(self.value, DPTArray):
-            return 1 + len(self.value.value)
-        raise TypeError()
+        return 1 + len(self.value.value)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> GroupValueWrite:
         """Parse/deserialize from KNX/IP raw data."""
         if len(raw) == 2:
-            self.value = DPTBinary(raw[1] & DPTBinary.APCI_BITMASK)
-        else:
-            self.value = DPTArray(raw[2:])
+            return cls(value=DPTBinary(raw[1] & DPTBinary.APCI_BITMASK))
+
+        return cls(value=DPTArray(raw[2:]))
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
         if isinstance(self.value, DPTBinary):
             return encode_cmd_and_payload(self.CODE, encoded_payload=self.value.value)
-        if isinstance(self.value, DPTArray):
-            return encode_cmd_and_payload(
-                self.CODE, appended_payload=bytes(self.value.value)
-            )
-        raise TypeError()
+
+        return encode_cmd_and_payload(
+            self.CODE, appended_payload=bytes(self.value.value)
+        )
 
     def __str__(self) -> str:
         """Return object as readable string."""
@@ -284,7 +287,7 @@ class GroupValueResponse(APCI):
 
     CODE = APCIService.GROUP_RESPONSE
 
-    def __init__(self, value: DPTBinary | DPTArray | None = None) -> None:
+    def __init__(self, value: DPTBinary | DPTArray) -> None:
         """Initialize a new instance of GroupValueResponse."""
         self.value = value
 
@@ -292,26 +295,22 @@ class GroupValueResponse(APCI):
         """Get length of APCI payload."""
         if isinstance(self.value, DPTBinary):
             return 1
-        if isinstance(self.value, DPTArray):
-            return 1 + len(self.value.value)
-        raise TypeError()
+        return 1 + len(self.value.value)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> GroupValueResponse:
         """Parse/deserialize from KNX/IP raw data."""
         if len(raw) == 2:
-            self.value = DPTBinary(raw[1] & DPTBinary.APCI_BITMASK)
-        else:
-            self.value = DPTArray(raw[2:])
+            return cls(value=DPTBinary(raw[1] & DPTBinary.APCI_BITMASK))
+        return cls(value=DPTArray(raw[2:]))
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
         if isinstance(self.value, DPTBinary):
             return encode_cmd_and_payload(self.CODE, encoded_payload=self.value.value)
-        if isinstance(self.value, DPTArray):
-            return encode_cmd_and_payload(
-                self.CODE, appended_payload=bytes(self.value.value)
-            )
-        raise TypeError()
+        return encode_cmd_and_payload(
+            self.CODE, appended_payload=bytes(self.value.value)
+        )
 
     def __str__(self) -> str:
         """Return object as readable string."""
@@ -327,24 +326,19 @@ class IndividualAddressWrite(APCI):
 
     CODE = APCIService.INDIVIDUAL_ADDRESS_WRITE
 
-    def __init__(
-        self,
-        address: IndividualAddress | None = None,
-    ) -> None:
+    def __init__(self, address: IndividualAddress) -> 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:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> IndividualAddressWrite:
         """Parse/deserialize from KNX/IP raw data."""
         (raw_address,) = struct.unpack("!H", raw[2:])
-        self.address = IndividualAddress(raw_address)
+        return cls(address=IndividualAddress(raw_address))
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -364,11 +358,11 @@ class IndividualAddressRead(APCI):
         """Get length of APCI payload."""
         return 1
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> IndividualAddressRead:
         """Parse/deserialize from KNX/IP raw data."""
-
         # Nothing to parse, but must be implemented explicitly.
-        return
+        return cls()
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -393,11 +387,11 @@ class IndividualAddressResponse(APCI):
         """Get length of APCI payload."""
         return 1
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> IndividualAddressResponse:
         """Parse/deserialize from KNX/IP raw data."""
-
         # Nothing to parse, but must be implemented explicitly.
-        return
+        return cls()
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -417,7 +411,7 @@ class ADCRead(APCI):
 
     CODE = APCIService.ADC_READ
 
-    def __init__(self, channel: int = 0, count: int = 1) -> None:
+    def __init__(self, channel: int, count: int = 1) -> None:
         """Initialize a new instance of ADCRead."""
         self.channel = channel
         self.count = count
@@ -426,11 +420,15 @@ class ADCRead(APCI):
         """Get length of APCI payload."""
         return 2
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> ADCRead:
         """Parse/deserialize from KNX/IP raw data."""
-        channel, self.count = struct.unpack("!BB", raw[1:])
+        channel, count = struct.unpack("!BB", raw[1:])
 
-        self.channel = channel & DPTBinary.APCI_BITMASK
+        return cls(
+            channel=channel & DPTBinary.APCI_BITMASK,
+            count=count,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -454,7 +452,7 @@ class ADCResponse(APCI):
 
     CODE = APCIService.ADC_RESPONSE
 
-    def __init__(self, channel: int = 0, count: int = 0, value: int = 0) -> None:
+    def __init__(self, channel: int, count: int = 1, value: int = 0) -> None:
         """Initialize a new instance of ADCResponse."""
         self.channel = channel
         self.count = count
@@ -464,11 +462,16 @@ class ADCResponse(APCI):
         """Get length of APCI payload."""
         return 4
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> ADCResponse:
         """Parse/deserialize from KNX/IP raw data."""
-        channel, self.count, self.value = struct.unpack("!BBH", raw[1:])
+        channel, count, value = struct.unpack("!BBH", raw[1:])
 
-        self.channel = channel & DPTBinary.APCI_BITMASK
+        return cls(
+            channel=channel & DPTBinary.APCI_BITMASK,
+            count=count,
+            value=value,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -492,7 +495,7 @@ class MemoryRead(APCI):
 
     CODE = APCIService.MEMORY_READ
 
-    def __init__(self, address: int = 0, count: int = 1) -> None:
+    def __init__(self, address: int, count: int = 1) -> None:
         """Initialize a new instance of MemoryRead."""
         self.address = address
         self.count = count
@@ -501,11 +504,15 @@ class MemoryRead(APCI):
         """Get length of APCI payload."""
         return 3
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> MemoryRead:
         """Parse/deserialize from KNX/IP raw data."""
-        count, self.address = struct.unpack("!BH", raw[1:])
+        count, address = struct.unpack("!BH", raw[1:])
 
-        self.count = count & DPTBinary.APCI_BITMASK
+        return cls(
+            address=address,
+            count=count & DPTBinary.APCI_BITMASK,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -534,14 +541,10 @@ class MemoryWrite(APCI):
 
     CODE = APCIService.MEMORY_WRITE
 
-    def __init__(
-        self, address: int = 0, count: int = 1, data: bytes | None = None
-    ) -> None:
+    def __init__(self, address: int, data: bytes, count: int | None = None) -> None:
         """Initialize a new instance of MemoryWrite."""
-
-        if data is None:
-            data = bytearray()
-
+        if count is None:
+            count = len(data)
         self.address = address
         self.count = count
         self.data = data
@@ -550,13 +553,18 @@ class MemoryWrite(APCI):
         """Get length of APCI payload."""
         return 3 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> MemoryWrite:
         """Parse/deserialize from KNX/IP raw data."""
         size = len(raw) - 4
 
-        count, self.address, self.data = struct.unpack(f"!BH{size}s", raw[1:])
+        count, address, data = struct.unpack(f"!BH{size}s", raw[1:])
 
-        self.count = count & DPTBinary.APCI_BITMASK
+        return cls(
+            count=count & DPTBinary.APCI_BITMASK,
+            address=address,
+            data=data,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -586,14 +594,10 @@ class MemoryResponse(APCI):
 
     CODE = APCIService.MEMORY_RESPONSE
 
-    def __init__(
-        self, address: int = 0, count: int = 1, data: bytes | None = None
-    ) -> None:
+    def __init__(self, address: int, data: bytes, count: int | None = None) -> None:
         """Initialize a new instance of MemoryResponse."""
-
-        if data is None:
-            data = bytearray()
-
+        if count is None:
+            count = len(data)
         self.address = address
         self.count = count
         self.data = data
@@ -602,13 +606,18 @@ class MemoryResponse(APCI):
         """Get length of APCI payload."""
         return 3 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> MemoryResponse:
         """Parse/deserialize from KNX/IP raw data."""
         size = len(raw) - 4
 
-        count, self.address, self.data = struct.unpack(f"!BH{size}s", raw[1:])
+        count, address, data = struct.unpack(f"!BH{size}s", raw[1:])
 
-        self.count = count & DPTBinary.APCI_BITMASK
+        return cls(
+            count=count & DPTBinary.APCI_BITMASK,
+            address=address,
+            data=data,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -646,9 +655,10 @@ class DeviceDescriptorRead(APCI):
         """Get length of APCI payload."""
         return 1
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> DeviceDescriptorRead:
         """Parse/deserialize from KNX/IP raw data."""
-        self.descriptor = raw[1] & 0x3F
+        return cls(descriptor=raw[1] & 0x3F)
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -680,11 +690,12 @@ class DeviceDescriptorResponse(APCI):
         """Get length of APCI payload."""
         return 3
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> DeviceDescriptorResponse:
         """Parse/deserialize from KNX/IP raw data."""
-        self.descriptor, self.value = struct.unpack("!BH", raw[1:])
+        descriptor, value = struct.unpack("!BH", raw[1:])
 
-        self.descriptor = self.descriptor & 0x3F
+        return cls(descriptor=descriptor & 0x3F, value=value)
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -718,11 +729,11 @@ class Restart(APCI):
         """Get length of APCI payload."""
         return 1
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> Restart:
         """Parse/deserialize from KNX/IP raw data."""
-
         # Nothing to parse, but must be implemented explicitly.
-        return
+        return cls()
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -751,12 +762,15 @@ class UserMemoryRead(APCI):
         """Get length of APCI payload."""
         return 4
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> UserMemoryRead:
         """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
+        return cls(
+            count=byte0 & 0x0F,
+            address=(((byte0 & 0xF0) >> 4) << 16) + address,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -786,14 +800,10 @@ class UserMemoryWrite(APCI):
 
     CODE = APCIUserService.USER_MEMORY_WRITE
 
-    def __init__(
-        self, address: int = 0, count: int = 1, data: bytes | None = None
-    ) -> None:
+    def __init__(self, address: int, data: bytes, count: int | None = None) -> None:
         """Initialize a new instance of UserMemoryWrite."""
-
-        if data is None:
-            data = bytearray()
-
+        if count is None:
+            count = len(data)
         self.address = address
         self.count = count
         self.data = data
@@ -802,14 +812,18 @@ class UserMemoryWrite(APCI):
         """Get length of APCI payload."""
         return 4 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> UserMemoryWrite:
         """Parse/deserialize from KNX/IP raw data."""
         size = len(raw) - 5
 
-        byte0, address, self.data = struct.unpack(f"!BH{size}s", raw[2:])
+        byte0, address, data = struct.unpack(f"!BH{size}s", raw[2:])
 
-        self.count = byte0 & 0x0F
-        self.address = (((byte0 & 0xF0) >> 4) << 16) + address
+        return cls(
+            address=(((byte0 & 0xF0) >> 4) << 16) + address,
+            data=data,
+            count=byte0 & 0x0F,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -840,14 +854,10 @@ class UserMemoryResponse(APCI):
 
     CODE = APCIUserService.USER_MEMORY_RESPONSE
 
-    def __init__(
-        self, address: int = 0, count: int = 1, data: bytes | None = None
-    ) -> None:
+    def __init__(self, address: int, data: bytes, count: int | None = None) -> None:
         """Initialize a new instance of UserMemoryResponse."""
-
-        if data is None:
-            data = bytearray()
-
+        if count is None:
+            count = len(data)
         self.address = address
         self.count = count
         self.data = data
@@ -856,14 +866,18 @@ class UserMemoryResponse(APCI):
         """Get length of APCI payload."""
         return 4 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> UserMemoryResponse:
         """Parse/deserialize from KNX/IP raw data."""
         size = len(raw) - 5
 
-        byte0, address, self.data = struct.unpack(f"!BH{size}s", raw[2:])
+        byte0, address, data = struct.unpack(f"!BH{size}s", raw[2:])
 
-        self.count = byte0 & 0x0F
-        self.address = (((byte0 & 0xF0) >> 4) << 16) + address
+        return cls(
+            address=(((byte0 & 0xF0) >> 4) << 16) + address,
+            data=data,
+            count=byte0 & 0x0F,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -894,11 +908,11 @@ class UserManufacturerInfoRead(APCI):
         """Get length of APCI payload."""
         return 1
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> UserManufacturerInfoRead:
         """Parse/deserialize from KNX/IP raw data."""
-
         # Nothing to parse, but must be implemented explicitly.
-        return
+        return cls()
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -927,9 +941,11 @@ class UserManufacturerInfoResponse(APCI):
         """Get length of APCI payload."""
         return 4
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> UserManufacturerInfoResponse:
         """Parse/deserialize from KNX/IP raw data."""
-        self.manufacturer_id, self.data = struct.unpack("!B2s", raw[2:])
+        manufacturer_id, data = struct.unpack("!B2s", raw[2:])
+        return cls(manufacturer_id=manufacturer_id, data=data)
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -948,12 +964,9 @@ class FunctionPropertyCommand(APCI):
     CODE = APCIUserService.FUNCTION_PROPERTY_COMMAND
 
     def __init__(
-        self, object_index: int = 0, property_id: int = 0, data: bytes | None = None
+        self, object_index: int = 0, property_id: int = 0, data: bytes = b""
     ) -> None:
         """Initialize a new instance of FunctionPropertyCommand."""
-        if data is None:
-            data = b""
-
         self.object_index = object_index
         self.property_id = property_id
         self.data = data
@@ -962,12 +975,15 @@ class FunctionPropertyCommand(APCI):
         """Get length of APCI payload."""
         return 3 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> FunctionPropertyCommand:
         """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:]
+        object_index, property_id, data = struct.unpack(f"!BB{size}s", raw[2:])
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            data=data,
         )
 
     def to_knx(self) -> bytearray:
@@ -990,12 +1006,9 @@ class FunctionPropertyStateRead(APCI):
     CODE = APCIUserService.FUNCTION_PROPERTY_STATE_READ
 
     def __init__(
-        self, object_index: int = 0, property_id: int = 0, data: bytes | None = None
+        self, object_index: int = 0, property_id: int = 0, data: bytes = b""
     ) -> None:
         """Initialize a new instance of FunctionPropertyStateRead."""
-        if data is None:
-            data = b""
-
         self.object_index = object_index
         self.property_id = property_id
         self.data = data
@@ -1004,12 +1017,15 @@ class FunctionPropertyStateRead(APCI):
         """Get length of APCI payload."""
         return 3 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> FunctionPropertyStateRead:
         """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:]
+        object_index, property_id, data = struct.unpack(f"!BB{size}s", raw[2:])
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            data=data,
         )
 
     def to_knx(self) -> bytearray:
@@ -1029,19 +1045,16 @@ class FunctionPropertyStateRead(APCI):
 class FunctionPropertyStateResponse(APCI):
     """FunctionPropertyStateResponse service."""
 
-    CODE = APCIUserService.FUNCTION_PROPERTY_STATE_READ
+    CODE = APCIUserService.FUNCTION_PROPERTY_STATE_RESPONSE
 
     def __init__(
         self,
         object_index: int = 0,
         property_id: int = 0,
         return_code: int = 0,
-        data: bytes | None = None,
+        data: bytes = b"",
     ) -> None:
         """Initialize a new instance of FunctionPropertyStateResponse."""
-        if data is None:
-            data = b""
-
         self.object_index = object_index
         self.property_id = property_id
         self.return_code = return_code
@@ -1051,16 +1064,22 @@ class FunctionPropertyStateResponse(APCI):
         """Get length of APCI payload."""
         return 4 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> FunctionPropertyStateResponse:
         """Parse/deserialize from KNX/IP raw data."""
         size = len(raw) - 5
-
         (
-            self.object_index,
-            self.property_id,
-            self.return_code,
-            self.data,
+            object_index,
+            property_id,
+            return_code,
+            data,
         ) = struct.unpack(f"!BBB{size}s", raw[2:])
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            return_code=return_code,
+            data=data,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1093,9 +1112,11 @@ class AuthorizeRequest(APCI):
         """Get length of APCI payload."""
         return 6
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> AuthorizeRequest:
         """Parse/deserialize from KNX/IP raw data."""
-        _, self.key = struct.unpack("!BI", raw[2:])
+        _, key = struct.unpack("!BI", raw[2:])
+        return cls(key=key)
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1121,9 +1142,11 @@ class AuthorizeResponse(APCI):
         """Get length of APCI payload."""
         return 2
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> AuthorizeResponse:
         """Parse/deserialize from KNX/IP raw data."""
-        (self.level,) = struct.unpack("!B", raw[2:])
+        (level,) = struct.unpack("!B", raw[2:])
+        return cls(level=level)
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1162,16 +1185,21 @@ class PropertyValueRead(APCI):
         """Get length of APCI payload."""
         return 5
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> PropertyValueRead:
         """Parse/deserialize from KNX/IP raw data."""
         (
-            self.object_index,
-            self.property_id,
+            object_index,
+            property_id,
             count,
-            self.start_index,
+            start_index,
         ) = struct.unpack("!BBBB", raw[2:])
-
-        self.count = count >> 4
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            count=count >> 4,
+            start_index=(count & 0xF) * 256 + start_index,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1182,8 +1210,8 @@ class PropertyValueRead(APCI):
             "!BBBB",
             self.object_index,
             self.property_id,
-            self.count << 4,
-            self.start_index,
+            (self.count << 4) + (self.start_index >> 8),
+            self.start_index & 0xFF,
         )
 
         return encode_cmd_and_payload(self.CODE, appended_payload=payload)
@@ -1215,13 +1243,9 @@ class PropertyValueWrite(APCI):
         property_id: int = 0,
         count: int = 1,
         start_index: int = 0,
-        data: bytes | None = None,
+        data: bytes = b"",
     ) -> 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
@@ -1238,32 +1262,35 @@ class PropertyValueWrite(APCI):
             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.count << 4) + (self.start_index >> 8),
+            self.start_index & 0xFF,
             self.data,
         )
 
         return encode_cmd_and_payload(self.CODE, appended_payload=payload)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> PropertyValueWrite:
         """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
+            object_index,
+            property_id,
+            count,
+            start_index,
+            data,
+        ) = struct.unpack(f"!BBBB{size}s", raw[2:])
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            count=count >> 4,
+            start_index=(count & 0xF) * 256 + start_index,
+            data=data,
+        )
 
     def __str__(self) -> str:
         """Return object as readable string."""
@@ -1294,13 +1321,9 @@ class PropertyValueResponse(APCI):
         property_id: int = 0,
         count: int = 1,
         start_index: int = 0,
-        data: bytes | None = None,
+        data: bytes = b"",
     ) -> 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
@@ -1311,20 +1334,24 @@ class PropertyValueResponse(APCI):
         """Get length of APCI payload."""
         return 5 + len(self.data)
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> PropertyValueResponse:
         """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
+            object_index,
+            property_id,
+            count,
+            start_index,
+            data,
+        ) = struct.unpack(f"!BBBB{size}s", raw[2:])
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            count=count >> 4,
+            start_index=(count & 0xF) * 256 + start_index,
+            data=data,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1332,13 +1359,12 @@ class PropertyValueResponse(APCI):
             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.count << 4) + (self.start_index >> 8),
+            self.start_index & 0xFF,
             self.data,
         )
 
@@ -1374,10 +1400,14 @@ class PropertyDescriptionRead(APCI):
         """Get length of APCI payload."""
         return 4
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> PropertyDescriptionRead:
         """Parse/deserialize from KNX/IP raw data."""
-        self.object_index, self.property_id, self.property_index = struct.unpack(
-            "!BBB", raw[2:]
+        object_index, property_id, property_index = struct.unpack("!BBB", raw[2:])
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            property_index=property_index,
         )
 
     def to_knx(self) -> bytearray:
@@ -1419,18 +1449,26 @@ class PropertyDescriptionResponse(APCI):
         """Get length of APCI payload."""
         return 8
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> PropertyDescriptionResponse:
         """Parse/deserialize from KNX/IP raw data."""
         (
-            self.object_index,
-            self.property_id,
-            self.property_index,
-            self.type,
+            object_index,
+            property_id,
+            property_index,
+            type_,
             max_count,
-            self.access,
+            access,
         ) = struct.unpack("!BBBBHB", raw[2:])
 
-        self.max_count = max_count & 0x0FFF
+        return cls(
+            object_index=object_index,
+            property_id=property_id,
+            property_index=property_index,
+            type_=type_,
+            max_count=max_count & 0x0FFF,
+            access=access,
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1459,20 +1497,19 @@ class IndividualAddressSerialRead(APCI):
 
     CODE = APCIExtendedService.INDIVIDUAL_ADDRESS_SERIAL_READ
 
-    def __init__(self, serial: bytes | None = None) -> None:
+    def __init__(self, serial: bytes) -> 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:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> IndividualAddressSerialRead:
         """Parse/deserialize from KNX/IP raw data."""
-        (self.serial,) = struct.unpack("!6s", raw[2:])
+        (serial,) = struct.unpack("!6s", raw[2:])
+        return cls(serial=serial)
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1495,15 +1532,10 @@ class IndividualAddressSerialResponse(APCI):
 
     def __init__(
         self,
-        serial: bytes | None = None,
-        address: IndividualAddress | None = None,
+        serial: bytes,
+        address: IndividualAddress,
     ) -> 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
 
@@ -1511,10 +1543,14 @@ class IndividualAddressSerialResponse(APCI):
         """Get length of APCI payload."""
         return 11
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> IndividualAddressSerialResponse:
         """Parse/deserialize from KNX/IP raw data."""
-        self.serial, raw_address, _ = struct.unpack("!6sHH", raw[2:])
-        self.address = IndividualAddress(raw_address)
+        serial, raw_address, _ = struct.unpack("!6sHH", raw[2:])
+        return cls(
+            serial=serial,
+            address=IndividualAddress(raw_address),
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
@@ -1537,15 +1573,10 @@ class IndividualAddressSerialWrite(APCI):
 
     def __init__(
         self,
-        serial: bytes | None = None,
-        address: IndividualAddress | None = None,
+        serial: bytes,
+        address: IndividualAddress,
     ) -> 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
 
@@ -1553,10 +1584,14 @@ class IndividualAddressSerialWrite(APCI):
         """Get length of APCI payload."""
         return 13
 
-    def from_knx(self, raw: bytes) -> None:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> IndividualAddressSerialWrite:
         """Parse/deserialize from KNX/IP raw data."""
-        self.serial, raw_address, _ = struct.unpack("!6sHI", raw[2:])
-        self.address = IndividualAddress(raw_address)
+        serial, raw_address, _ = struct.unpack("!6sHI", raw[2:])
+        return cls(
+            serial=serial,
+            address=IndividualAddress(raw_address),
+        )
 
     def to_knx(self) -> bytearray:
         """Serialize to KNX/IP raw data."""
diff --git a/xknx/tools/group_communication.py b/xknx/tools/group_communication.py
index 41e54538..5de28c5f 100644
--- a/xknx/tools/group_communication.py
+++ b/xknx/tools/group_communication.py
@@ -82,7 +82,6 @@ async def read_group_value(
     response = await value_reader.read()
     if response is not None:
         assert isinstance(response.payload, (GroupValueWrite, GroupValueResponse))
-        assert response.payload.value is not None
         if transcoder is not None:
             return transcoder.from_knx(response.payload.value.value)  # type: ignore[arg-type]
         return response.payload.value.value


Test patch:
diff --git a/test/cemi_tests/cemi_frame_test.py b/test/cemi_tests/cemi_frame_test.py
index 3abf04a3..b82a81ce 100644
--- a/test/cemi_tests/cemi_frame_test.py
+++ b/test/cemi_tests/cemi_frame_test.py
@@ -69,7 +69,7 @@ def test_valid_tpci_control():
         ),
         (
             get_data(0x29, 0, 0, 0, 0, 1, 0x03C0, []),
-            r".*APCI not supported*",
+            r".*APDU not supported*",
         ),
     ],
 )
diff --git a/test/telegram_tests/apci_test.py b/test/telegram_tests/apci_test.py
index eecf0b93..14927fe1 100644
--- a/test/telegram_tests/apci_test.py
+++ b/test/telegram_tests/apci_test.py
@@ -47,89 +47,6 @@ from xknx.telegram.apci import (
 class TestAPCI:
     """Test class for APCI objects."""
 
-    def test_resolve_apci(self):
-        """Test resolve_apci for supported APCI services."""
-        test_cases = [
-            (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:
-            assert isinstance(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:
-            assert isinstance(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:
-            assert isinstance(APCI.resolve_apci(code), clazz)
-
     def test_resolve_apci_unsupported(self):
         """Test resolve_apci for unsupported services."""
 
@@ -137,13 +54,13 @@ class TestAPCI:
             ConversionError, match=r".*Class not implemented for APCI.*"
         ):
             # Unsupported user service.
-            APCI.resolve_apci(0x02C3)
+            APCI.from_knx(bytes((0x02, 0xC3)))
 
         with pytest.raises(
             ConversionError, match=r".*Class not implemented for APCI.*"
         ):
             # Unsupported extended service.
-            APCI.resolve_apci(0x03C0)
+            APCI.from_knx(bytes((0x03, 0xC0)))
 
 
 class TestGroupValueRead:
@@ -157,8 +74,7 @@ class TestGroupValueRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = GroupValueRead()
-        payload.from_knx(bytes([0x00, 0x00]))
+        payload = APCI.from_knx(bytes([0x00, 0x00]))
 
         assert payload == GroupValueRead()
 
@@ -186,19 +102,10 @@ class TestGroupValueWrite:
         assert payload_a.calculated_length() == 4
         assert payload_b.calculated_length() == 1
 
-    def test_calculated_length_exception(self):
-        """Test the test_calculated_length method for unsupported dpt."""
-        payload = GroupValueWrite(object())
-
-        with pytest.raises(TypeError):
-            payload.calculated_length()
-
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload_a = GroupValueWrite()
-        payload_a.from_knx(bytes([0x00, 0x80, 0x05, 0x04, 0x03, 0x02, 0x01]))
-        payload_b = GroupValueWrite()
-        payload_b.from_knx(bytes([0x00, 0x82]))
+        payload_a = APCI.from_knx(bytes([0x00, 0x80, 0x05, 0x04, 0x03, 0x02, 0x01]))
+        payload_b = APCI.from_knx(bytes([0x00, 0x82]))
 
         assert payload_a == GroupValueWrite(DPTArray((0x05, 0x04, 0x03, 0x02, 0x01)))
         assert payload_b == GroupValueWrite(DPTBinary(0x02))
@@ -212,13 +119,6 @@ class TestGroupValueWrite:
         assert payload_a.to_knx() == bytes([0x00, 0x80, 0x01, 0x02, 0x03])
         assert payload_b.to_knx() == bytes([0x00, 0x81])
 
-    def test_to_knx_exception(self):
-        """Test the to_knx method for unsupported dpt."""
-        payload = GroupValueWrite(object())
-
-        with pytest.raises(TypeError):
-            payload.to_knx()
-
     def test_str(self):
         """Test the __str__ method."""
         payload = GroupValueWrite(DPTBinary(1))
@@ -237,19 +137,10 @@ class TestGroupValueResponse:
         assert payload_a.calculated_length() == 4
         assert payload_b.calculated_length() == 1
 
-    def test_calculated_length_exception(self):
-        """Test the test_calculated_length method for unsupported dpt."""
-        payload = GroupValueResponse(object())
-
-        with pytest.raises(TypeError):
-            payload.calculated_length()
-
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload_a = GroupValueResponse()
-        payload_a.from_knx(bytes([0x00, 0x80, 0x05, 0x04, 0x03, 0x02, 0x01]))
-        payload_b = GroupValueResponse()
-        payload_b.from_knx(bytes([0x00, 0x82]))
+        payload_a = APCI.from_knx(bytes([0x00, 0x40, 0x05, 0x04, 0x03, 0x02, 0x01]))
+        payload_b = APCI.from_knx(bytes([0x00, 0x42]))
 
         assert payload_a == GroupValueResponse(DPTArray((0x05, 0x04, 0x03, 0x02, 0x01)))
         assert payload_b == GroupValueResponse(DPTBinary(0x02))
@@ -263,13 +154,6 @@ class TestGroupValueResponse:
         assert payload_a.to_knx() == bytes([0x00, 0x40, 0x01, 0x02, 0x03])
         assert payload_b.to_knx() == bytes([0x00, 0x41])
 
-    def test_to_knx_exception(self):
-        """Test the to_knx method for unsupported dpt."""
-        payload = GroupValueResponse(object())
-
-        with pytest.raises(TypeError):
-            payload.to_knx()
-
     def test_str(self):
         """Test the __str__ method."""
         payload = GroupValueResponse(DPTBinary(1))
@@ -282,14 +166,13 @@ class TestIndividualAddressWrite:
 
     def test_calculated_length(self):
         """Test the test_calculated_length method."""
-        payload = IndividualAddressWrite()
+        payload = IndividualAddressWrite(IndividualAddress("1.2.3"))
 
         assert payload.calculated_length() == 3
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = IndividualAddressWrite()
-        payload.from_knx(bytes([0x00, 0xC0, 0x12, 0x03]))
+        payload = APCI.from_knx(bytes([0x00, 0xC0, 0x12, 0x03]))
 
         assert payload == IndividualAddressWrite(IndividualAddress("1.2.3"))
 
@@ -317,8 +200,7 @@ class TestIndividualAddressRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = IndividualAddressRead()
-        payload.from_knx(bytes([0x01, 0x00]))
+        payload = APCI.from_knx(bytes([0x01, 0x00]))
 
         assert payload == IndividualAddressRead()
 
@@ -340,14 +222,13 @@ class TestIndividualAddressResponse:
 
     def test_calculated_length(self):
         """Test the test_calculated_length method."""
-        payload = IndividualAddressRead()
+        payload = IndividualAddressResponse()
 
         assert payload.calculated_length() == 1
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = IndividualAddressResponse()
-        payload.from_knx(bytes([0x01, 0x40]))
+        payload = APCI.from_knx(bytes([0x01, 0x40]))
 
         assert payload == IndividualAddressResponse()
 
@@ -369,14 +250,13 @@ class TestADCRead:
 
     def test_calculated_length(self):
         """Test the test_calculated_length method."""
-        payload = ADCRead()
+        payload = ADCRead(channel=2, count=4)
 
         assert payload.calculated_length() == 2
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = ADCRead()
-        payload.from_knx(bytes([0x01, 0x82, 0x04]))
+        payload = APCI.from_knx(bytes([0x01, 0x82, 0x04]))
 
         assert payload == ADCRead(channel=2, count=4)
 
@@ -398,14 +278,13 @@ class TestADCResponse:
 
     def test_calculated_length(self):
         """Test the test_calculated_length method."""
-        payload = ADCResponse()
+        payload = ADCResponse(channel=2, count=4, value=1023)
 
         assert 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]))
+        payload = APCI.from_knx(bytes([0x01, 0xC2, 0x04, 0x03, 0xFF]))
 
         assert payload == ADCResponse(channel=2, count=4, value=1023)
 
@@ -427,14 +306,13 @@ class TestMemoryRead:
 
     def test_calculated_length(self):
         """Test the test_calculated_length method."""
-        payload = MemoryRead()
+        payload = MemoryRead(address=0x1234, count=11)
 
         assert payload.calculated_length() == 3
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = MemoryRead()
-        payload.from_knx(bytes([0x02, 0x0B, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x02, 0x0B, 0x12, 0x34]))
 
         assert payload == MemoryRead(address=0x1234, count=11)
 
@@ -474,8 +352,7 @@ class TestMemoryWrite:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = MemoryWrite()
-        payload.from_knx(bytes([0x02, 0x83, 0x12, 0x34, 0xAA, 0xBB, 0xCC]))
+        payload = APCI.from_knx(bytes([0x02, 0x83, 0x12, 0x34, 0xAA, 0xBB, 0xCC]))
 
         assert payload == MemoryWrite(
             address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])
@@ -523,8 +400,7 @@ class TestMemoryResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = MemoryResponse()
-        payload.from_knx(bytes([0x02, 0x43, 0x12, 0x34, 0xAA, 0xBB, 0xCC]))
+        payload = APCI.from_knx(bytes([0x02, 0x43, 0x12, 0x34, 0xAA, 0xBB, 0xCC]))
 
         assert payload == MemoryResponse(
             address=0x1234, count=3, data=bytes([0xAA, 0xBB, 0xCC])
@@ -577,8 +453,7 @@ class TestDeviceDescriptorRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = DeviceDescriptorRead()
-        payload.from_knx(bytes([0x03, 0x0D]))
+        payload = APCI.from_knx(bytes([0x03, 0x0D]))
 
         assert payload == DeviceDescriptorRead(13)
 
@@ -613,8 +488,7 @@ class TestDeviceDescriptorResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = DeviceDescriptorResponse()
-        payload.from_knx(bytes([0x03, 0x4D, 0x00, 0x7B]))
+        payload = APCI.from_knx(bytes([0x03, 0x4D, 0x00, 0x7B]))
 
         assert payload == DeviceDescriptorResponse(descriptor=13, value=123)
 
@@ -626,7 +500,7 @@ class TestDeviceDescriptorResponse:
 
     def test_to_knx_conversion_error(self):
         """Test the to_knx method for conversion errors."""
-        payload = DeviceDescriptorRead(255)
+        payload = DeviceDescriptorResponse(255)
 
         with pytest.raises(ConversionError, match=r".*Descriptor.*"):
             payload.to_knx()
@@ -649,8 +523,7 @@ class TestUserMemoryRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = UserMemoryRead()
-        payload.from_knx(bytes([0x02, 0xC0, 0x1B, 0x23, 0x45]))
+        payload = APCI.from_knx(bytes([0x02, 0xC0, 0x1B, 0x23, 0x45]))
 
         assert payload == UserMemoryRead(address=0x12345, count=11)
 
@@ -692,8 +565,7 @@ class TestUserMemoryWrite:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = UserMemoryWrite()
-        payload.from_knx(bytes([0x02, 0xC2, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC]))
+        payload = APCI.from_knx(bytes([0x02, 0xC2, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC]))
 
         assert payload == UserMemoryWrite(
             address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
@@ -750,8 +622,7 @@ class TestUserMemoryResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = UserMemoryResponse()
-        payload.from_knx(bytes([0x02, 0xC1, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC]))
+        payload = APCI.from_knx(bytes([0x02, 0xC1, 0x13, 0x23, 0x45, 0xAA, 0xBB, 0xCC]))
 
         assert payload == UserMemoryResponse(
             address=0x12345, count=3, data=bytes([0xAA, 0xBB, 0xCC])
@@ -806,8 +677,7 @@ class TestRestart:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = Restart()
-        payload.from_knx(bytes([0x03, 0x80]))
+        payload = APCI.from_knx(bytes([0x03, 0x80]))
 
         assert payload == Restart()
 
@@ -835,8 +705,7 @@ class TestUserManufacturerInfoRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = UserManufacturerInfoRead()
-        payload.from_knx(bytes([0x02, 0xC5]))
+        payload = APCI.from_knx(bytes([0x02, 0xC5]))
 
         assert payload == UserManufacturerInfoRead()
 
@@ -864,8 +733,7 @@ class TestUserManufacturerInfoResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = UserManufacturerInfoResponse()
-        payload.from_knx(bytes([0x02, 0xC6, 0x7B, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x02, 0xC6, 0x7B, 0x12, 0x34]))
 
         assert payload == UserManufacturerInfoResponse(
             manufacturer_id=123, data=b"\x12\x34"
@@ -900,8 +768,7 @@ class TestFunctionPropertyCommand:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = FunctionPropertyCommand()
-        payload.from_knx(bytes([0x02, 0xC7, 0x01, 0x04, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x02, 0xC7, 0x01, 0x04, 0x12, 0x34]))
 
         assert payload == FunctionPropertyCommand(
             object_index=1, property_id=4, data=b"\x12\x34"
@@ -940,8 +807,7 @@ class TestFunctionPropertyStateRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = FunctionPropertyStateRead()
-        payload.from_knx(bytes([0x02, 0xC8, 0x01, 0x04, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x02, 0xC8, 0x01, 0x04, 0x12, 0x34]))
 
         assert payload == FunctionPropertyStateRead(
             object_index=1, property_id=4, data=b"\x12\x34"
@@ -980,8 +846,7 @@ class TestFunctionPropertyStateResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = FunctionPropertyStateResponse()
-        payload.from_knx(bytes([0x02, 0xC8, 0x01, 0x04, 0x08, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x02, 0xC9, 0x01, 0x04, 0x08, 0x12, 0x34]))
 
         assert payload == FunctionPropertyStateResponse(
             object_index=1, property_id=4, return_code=8, data=b"\x12\x34"
@@ -993,7 +858,7 @@ class TestFunctionPropertyStateResponse:
             object_index=1, property_id=4, return_code=8, data=b"\x12\x34"
         )
 
-        assert payload.to_knx() == bytes([0x02, 0xC8, 0x01, 0x04, 0x08, 0x12, 0x34])
+        assert payload.to_knx() == bytes([0x02, 0xC9, 0x01, 0x04, 0x08, 0x12, 0x34])
 
     def test_str(self):
         """Test the __str__ method."""
@@ -1018,8 +883,7 @@ class TestAuthorizeRequest:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = AuthorizeRequest()
-        payload.from_knx(bytes([0x03, 0xD1, 0x00, 0x00, 0xBC, 0x61, 0x4E]))
+        payload = APCI.from_knx(bytes([0x03, 0xD1, 0x00, 0x00, 0xBC, 0x61, 0x4E]))
 
         assert payload == AuthorizeRequest(key=12345678)
 
@@ -1047,8 +911,7 @@ class TestAuthorizeResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = AuthorizeResponse()
-        payload.from_knx(bytes([0x03, 0xD2, 0x7B]))
+        payload = APCI.from_knx(bytes([0x03, 0xD2, 0x7B]))
 
         assert payload == AuthorizeResponse(level=123)
 
@@ -1078,8 +941,7 @@ class TestPropertyValueRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = PropertyValueRead()
-        payload.from_knx(bytes([0x03, 0xD5, 0x01, 0x04, 0x20, 0x08]))
+        payload = APCI.from_knx(bytes([0x03, 0xD5, 0x01, 0x04, 0x20, 0x08]))
 
         assert payload == PropertyValueRead(
             object_index=1, property_id=4, count=2, start_index=8
@@ -1127,8 +989,7 @@ class TestPropertyValueWrite:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = PropertyValueWrite()
-        payload.from_knx(bytes([0x03, 0xD7, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x03, 0xD7, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34]))
 
         assert payload == PropertyValueWrite(
             object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
@@ -1178,8 +1039,7 @@ class TestPropertyValueResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = PropertyValueResponse()
-        payload.from_knx(bytes([0x03, 0xD6, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34]))
+        payload = APCI.from_knx(bytes([0x03, 0xD6, 0x01, 0x04, 0x20, 0x08, 0x12, 0x34]))
 
         assert payload == PropertyValueResponse(
             object_index=1, property_id=4, count=2, start_index=8, data=b"\x12\x34"
@@ -1229,8 +1089,7 @@ class TestPropertyDescriptionRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = PropertyDescriptionRead()
-        payload.from_knx(bytes([0x03, 0xD8, 0x01, 0x04, 0x08]))
+        payload = APCI.from_knx(bytes([0x03, 0xD8, 0x01, 0x04, 0x08]))
 
         assert payload == PropertyDescriptionRead(
             object_index=1, property_id=4, property_index=8
@@ -1274,8 +1133,9 @@ class TestPropertyDescriptionResponse:
 
     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]))
+        payload = APCI.from_knx(
+            bytes([0x03, 0xD9, 0x01, 0x04, 0x08, 0x03, 0x00, 0x05, 0x07])
+        )
 
         assert payload == PropertyDescriptionResponse(
             object_index=1,
@@ -1343,8 +1203,7 @@ class TestIndividualAddressSerialRead:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = IndividualAddressSerialRead()
-        payload.from_knx(bytes([0x03, 0xDC, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33]))
+        payload = APCI.from_knx(bytes([0x03, 0xDC, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33]))
 
         assert payload == IndividualAddressSerialRead(b"\xaa\xbb\xcc\x11\x22\x33")
 
@@ -1376,8 +1235,7 @@ class TestIndividualAddressSerialResponse:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = IndividualAddressSerialResponse()
-        payload.from_knx(
+        payload = APCI.from_knx(
             bytes(
                 [0x03, 0xDD, 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33, 0x12, 0x03, 0x00, 0x00]
             )
@@ -1422,8 +1280,7 @@ class TestIndividualAddressSerialWrite:
 
     def test_from_knx(self):
         """Test the from_knx method."""
-        payload = IndividualAddressSerialWrite()
-        payload.from_knx(
+        payload = APCI.from_knx(
             bytes(
                 [
                     0x03,