Repository: XKNX/xknx
Pull request #1652 (2025-02-08 09:41:21)

Add `DPTBase.get_dpt` DPT definition helper classmethod
<!--
  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.
-->
This accepts int, string, dict (`{"main": int, "sub": int | None}`) or DPT classes. It raises ValueError if no usable subclass is found.

Examples:

```py
x = DPTBase.get_dpt(16)
reveal_type(x)  # -> type[DPTBase]

y = DPTNumeric.get_dpt(16)  # raises ValueError since DPT 16 is not Numeric
reveal_type(y)  # -> type[DPTNumeric]

DPTBase.get_dpt(DPTNumeric)  # raises ValueError since DPTNumeric is abstract (not a DPT definition, but a category of definitions)

DPTBase.get_dpt(DPTString)  # -> DPTString

DPTEnum.get_dpt({"main": 1, "sub": 8})  # -> DPTUpDown
```

cc @philippwaller 
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)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests

## Checklist

- [ ] The documentation has been adjusted accordingly
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog (docs/changelog.md)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/XKNX/xknx/pull/1652?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.93%. Comparing base [(`6d545f7`)](https://app.codecov.io/gh/XKNX/xknx/commit/6d545f7529e3be48078f7262bb5ca065928a9ad6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`71da2d0`)](https://app.codecov.io/gh/XKNX/xknx/commit/71da2d0e50f9c6064d9b927ecb0b5e4943155233?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX).

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/XKNX/xknx/pull/1652/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://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1652   +/-   ##
=======================================
  Coverage   96.92%   96.93%           
=======================================
  Files         160      160           
  Lines       10702    10718   +16     
=======================================
+ Hits        10373    10389   +16     
  Misses        329      329           
```

| [Files with missing lines](https://app.codecov.io/gh/XKNX/xknx/pull/1652?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/core/group\_address\_dpt.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fcore%2Fgroup_address_dpt.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jb3JlL2dyb3VwX2FkZHJlc3NfZHB0LnB5) | `97.82% <100.00%> (+0.04%)` | :arrow_up: |
| [xknx/devices/expose\_sensor.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fdevices%2Fexpose_sensor.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2V4cG9zZV9zZW5zb3IucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/devices/notification.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fdevices%2Fnotification.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL25vdGlmaWNhdGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/devices/numeric\_value.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fdevices%2Fnumeric_value.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL251bWVyaWNfdmFsdWUucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/devices/sensor.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fdevices%2Fsensor.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL3NlbnNvci5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0LnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/remote\_value/remote\_value\_sensor.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Fremote_value%2Fremote_value_sensor.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9yZW1vdGVfdmFsdWUvcmVtb3RlX3ZhbHVlX3NlbnNvci5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/tools/group\_communication.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Ftools%2Fgroup_communication.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90b29scy9ncm91cF9jb21tdW5pY2F0aW9uLnB5) | `97.67% <100.00%> (-0.33%)` | :arrow_down: |
| [xknx/typing/\_\_init\_\_.py](https://app.codecov.io/gh/XKNX/xknx/pull/1652?src=pr&el=tree&filepath=xknx%2Ftyping%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90eXBpbmcvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |

</details>

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index e83aa5d8..36fdaf98 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -9,6 +9,7 @@ nav_order: 2
 ### DPT
 
 - Add `DPTBase.dpt_number_str` and `DPTBase.dpt_name` classmethods for human readable DPT number (eg. "9.001") and class name (eg. "DPTTemperature (9.001)").
+- Add `DPTBase.get_dpt` classmethod to get a DPT class by its number, name or DPTBase type.
 
 # 3.5.0 Swing it 2025-01-28
 
diff --git a/xknx/core/group_address_dpt.py b/xknx/core/group_address_dpt.py
index 72d0bd1d..ff105c28 100644
--- a/xknx/core/group_address_dpt.py
+++ b/xknx/core/group_address_dpt.py
@@ -5,7 +5,7 @@ from __future__ import annotations
 from collections.abc import Mapping
 import logging
 
-from xknx.dpt.dpt import DPTBase, _DPTMainSubDict
+from xknx.dpt.dpt import DPTBase
 from xknx.exceptions import ConversionError, CouldNotParseAddress, CouldNotParseTelegram
 from xknx.telegram import Telegram, TelegramDecodedData
 from xknx.telegram.address import (
@@ -16,6 +16,7 @@ from xknx.telegram.address import (
     parse_device_group_address,
 )
 from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
+from xknx.typing import DPTParsable
 
 _GA_DPT_LOGGER = logging.getLogger("xknx.ga_dpt")
 
@@ -32,7 +33,7 @@ class GroupAddressDPT:
 
     def set(
         self,
-        ga_dpt: Mapping[DeviceAddressableType, int | str | _DPTMainSubDict],
+        ga_dpt: Mapping[DeviceAddressableType, DPTParsable],
     ) -> None:
         """Assign decoders to group addresses."""
         unknown_dpts = set()
diff --git a/xknx/devices/expose_sensor.py b/xknx/devices/expose_sensor.py
index 73c468b3..87912d1b 100644
--- a/xknx/devices/expose_sensor.py
+++ b/xknx/devices/expose_sensor.py
@@ -18,12 +18,14 @@ from collections.abc import Iterator
 from typing import TYPE_CHECKING, Any
 
 from xknx.core import Task
+from xknx.dpt import DPTBase
 from xknx.remote_value import (
     GroupAddressesType,
     RemoteValue,
     RemoteValueSensor,
     RemoteValueSwitch,
 )
+from xknx.typing import DPTParsable
 
 from .device import Device, DeviceCallbackType
 
@@ -41,7 +43,7 @@ class ExposeSensor(Device):
         name: str,
         group_address: GroupAddressesType = None,
         respond_to_read: bool = True,
-        value_type: int | str | None = None,
+        value_type: DPTParsable | type[DPTBase] | None = None,
         cooldown: float = 0,
         device_updated_cb: DeviceCallbackType[ExposeSensor] | None = None,
     ) -> None:
diff --git a/xknx/devices/notification.py b/xknx/devices/notification.py
index f6d42e12..f32ccf28 100644
--- a/xknx/devices/notification.py
+++ b/xknx/devices/notification.py
@@ -5,7 +5,9 @@ from __future__ import annotations
 from collections.abc import Iterator
 from typing import TYPE_CHECKING
 
+from xknx.dpt import DPTString
 from xknx.remote_value import GroupAddressesType, RemoteValueString
+from xknx.typing import DPTParsable
 
 from .device import Device, DeviceCallbackType
 
@@ -25,7 +27,7 @@ class Notification(Device):
         group_address_state: GroupAddressesType = None,
         respond_to_read: bool = False,
         sync_state: bool | int | float | str = True,
-        value_type: int | str | None = None,
+        value_type: DPTParsable | type[DPTString] | None = None,
         device_updated_cb: DeviceCallbackType[Notification] | None = None,
     ) -> None:
         """Initialize notification class."""
diff --git a/xknx/devices/numeric_value.py b/xknx/devices/numeric_value.py
index b1a607d6..29a495f1 100644
--- a/xknx/devices/numeric_value.py
+++ b/xknx/devices/numeric_value.py
@@ -13,7 +13,9 @@ from __future__ import annotations
 from collections.abc import Iterator
 from typing import TYPE_CHECKING
 
+from xknx.dpt import DPTNumeric
 from xknx.remote_value import GroupAddressesType, RemoteValueNumeric
+from xknx.typing import DPTParsable
 
 from .device import Device, DeviceCallbackType
 
@@ -33,7 +35,7 @@ class NumericValue(Device):
         group_address_state: GroupAddressesType = None,
         respond_to_read: bool = False,
         sync_state: bool | int | float | str = True,
-        value_type: int | str | None = None,
+        value_type: DPTParsable | type[DPTNumeric] | None = None,
         always_callback: bool = False,
         device_updated_cb: DeviceCallbackType[NumericValue] | None = None,
     ) -> None:
diff --git a/xknx/devices/sensor.py b/xknx/devices/sensor.py
index 51fd2d43..ab6b78a5 100644
--- a/xknx/devices/sensor.py
+++ b/xknx/devices/sensor.py
@@ -12,11 +12,13 @@ from __future__ import annotations
 from collections.abc import Iterator
 from typing import TYPE_CHECKING, Any
 
+from xknx.dpt import DPTBase
 from xknx.remote_value import (
     GroupAddressesType,
     RemoteValue,
     RemoteValueSensor,
 )
+from xknx.typing import DPTParsable
 
 from .device import Device, DeviceCallbackType
 
@@ -35,7 +37,7 @@ class Sensor(Device):
         group_address_state: GroupAddressesType = None,
         sync_state: bool | int | float | str = True,
         always_callback: bool = False,
-        value_type: int | str | None = None,
+        value_type: DPTParsable | type[DPTBase] | None = None,
         device_updated_cb: DeviceCallbackType[Sensor] | None = None,
     ) -> None:
         """Initialize Sensor class."""
diff --git a/xknx/dpt/dpt.py b/xknx/dpt/dpt.py
index 8b1fb34a..6429fcd5 100644
--- a/xknx/dpt/dpt.py
+++ b/xknx/dpt/dpt.py
@@ -8,21 +8,14 @@ from dataclasses import dataclass
 from enum import Enum
 from inspect import isabstract
 import struct
-from typing import Any, Generic, TypedDict, TypeVar, cast, final
+from typing import Any, Generic, TypeVar, cast, final
 
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
-from xknx.typing import Self
+from xknx.typing import DPTParsable, Self
 
 from .payload import DPTArray, DPTBinary
 
 
-class _DPTMainSubDict(TypedDict):
-    """DPT type dictionary in accordance to xknxproject DPTType data."""
-
-    main: int
-    sub: int | None
-
-
 class DPTBase(ABC):
     """
     Base class for KNX data point type transcoder.
@@ -183,9 +176,7 @@ class DPTBase(ABC):
         return None
 
     @classmethod
-    def parse_transcoder(
-        cls: type[Self], value_type: int | str | _DPTMainSubDict
-    ) -> type[Self] | None:
+    def parse_transcoder(cls: type[Self], value_type: DPTParsable) -> type[Self] | None:
         """
         Return Class reference of DPTBase subclass from value_type or DPT number.
 
@@ -223,6 +214,23 @@ class DPTBase(ABC):
                 return None
             return cls.transcoder_by_dpt(dpt_main=main, dpt_sub=_sub)
 
+    @classmethod
+    def get_dpt(cls: type[Self], value_type: DPTParsable | type[DPTBase]) -> type[Self]:
+        """
+        Return DPT class from value.
+
+        Raises ValueError if value_type can't be parsed to DPT class.
+        """
+        if isinstance(value_type, type):
+            if issubclass(value_type, cls) and not isabstract(value_type):
+                return value_type
+        else:
+            if transcoder := cls.parse_transcoder(value_type):
+                return transcoder
+        raise ValueError(
+            f"Invalid value type for base class {cls.__name__}: {value_type}"
+        )
+
 
 class DPTNumeric(DPTBase):
     """Base class for KNX data point types decoding numeric values."""
diff --git a/xknx/remote_value/remote_value_sensor.py b/xknx/remote_value/remote_value_sensor.py
index 3cb75d21..a75d9523 100644
--- a/xknx/remote_value/remote_value_sensor.py
+++ b/xknx/remote_value/remote_value_sensor.py
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, TypeVar
 
 from xknx.dpt import DPTBase, DPTNumeric, DPTString
 from xknx.exceptions import ConversionError
+from xknx.typing import DPTParsable
 
 from .remote_value import GroupAddressesType, RemoteValue, RVCallbackType
 
@@ -33,24 +34,25 @@ class _RemoteValueGeneric(RemoteValue[ValueT]):
         group_address: GroupAddressesType = None,
         group_address_state: GroupAddressesType = None,
         sync_state: bool | int | float | str = True,
-        value_type: int | str | None = None,
+        value_type: DPTParsable | type[DPTBase] | None = None,
         device_name: str | None = None,
         feature_name: str = "Value",
         after_update_cb: RVCallbackType[ValueT] | None = None,
     ) -> None:
         """Initialize RemoteValueSensor class."""
-        _dpt_class = (
-            self.dpt_base_class.parse_transcoder(value_type)
-            if value_type is not None
-            else self._default_dpt_class
-        )
-        if _dpt_class is None:
+        try:
+            if value_type is None:
+                if self._default_dpt_class is None:
+                    raise ValueError
+                self.dpt_class = self._default_dpt_class
+            else:
+                self.dpt_class = self.dpt_base_class.get_dpt(value_type)
+        except ValueError:
             raise ConversionError(
                 f"invalid value type for base class {self.dpt_base_class.dpt_name()}",
                 value_type=value_type,
                 device_name=device_name,
-            )
-        self.dpt_class = _dpt_class
+            ) from None
         super().__init__(
             xknx,
             group_address,
diff --git a/xknx/tools/group_communication.py b/xknx/tools/group_communication.py
index bb38f478..965db8c0 100644
--- a/xknx/tools/group_communication.py
+++ b/xknx/tools/group_communication.py
@@ -10,6 +10,7 @@ from xknx.dpt import DPTArray, DPTBase, DPTBinary
 from xknx.telegram import Telegram
 from xknx.telegram.address import DeviceAddressableType, parse_device_group_address
 from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
+from xknx.typing import DPTParsable
 
 if TYPE_CHECKING:
     from xknx.xknx import XKNX
@@ -34,7 +35,7 @@ def group_value_response(
     xknx: XKNX,
     group_address: DeviceAddressableType,
     value: Any,
-    value_type: int | str | type[DPTBase] | None = None,
+    value_type: DPTParsable | type[DPTBase] | None = None,
 ) -> None:
     """Send a GroupValueResponse telegram."""
     payload = _parse_payload(value, value_type)
@@ -55,7 +56,7 @@ def group_value_write(
     xknx: XKNX,
     group_address: DeviceAddressableType,
     value: Any,
-    value_type: int | str | type[DPTBase] | None = None,
+    value_type: DPTParsable | type[DPTBase] | None = None,
 ) -> None:
     """Send a GroupValueWrite telegram."""
     payload = _parse_payload(value, value_type)
@@ -75,7 +76,7 @@ def group_value_write(
 async def read_group_value(
     xknx: XKNX,
     group_address: DeviceAddressableType,
-    value_type: int | str | type[DPTBase] | None = None,
+    value_type: DPTParsable | type[DPTBase] | None = None,
 ) -> int | tuple[int, ...] | Any | None:
     """Read a value from a KNX group address."""
     transcoder = _parse_dpt(value_type)
@@ -89,24 +90,15 @@ async def read_group_value(
     return None
 
 
-def _parse_dpt(value_type: int | str | type[DPTBase] | None) -> type[DPTBase] | None:
+def _parse_dpt(value_type: DPTParsable | type[DPTBase] | None) -> type[DPTBase] | None:
     if value_type is None:
         return None
-    if isinstance(value_type, int | str):
-        if transcoder := DPTBase.parse_transcoder(value_type):
-            return transcoder
-    else:
-        try:
-            if issubclass(value_type, DPTBase):
-                return value_type
-        except TypeError:
-            pass
-    raise ValueError(f"Invalid value_type: {value_type}")
+    return DPTBase.get_dpt(value_type)
 
 
 def _parse_payload(
     value: Any,
-    value_type: int | str | type[DPTBase] | None = None,
+    value_type: DPTParsable | type[DPTBase] | None = None,
 ) -> DPTBinary | DPTArray:
     if isinstance(value, DPTArray | DPTBinary):
         return value
diff --git a/xknx/typing/__init__.py b/xknx/typing/__init__.py
index 515a17d9..9cc6bd81 100644
--- a/xknx/typing/__init__.py
+++ b/xknx/typing/__init__.py
@@ -2,7 +2,7 @@
 
 from collections.abc import Callable
 import sys
-from typing import TYPE_CHECKING, TypeVar
+from typing import TYPE_CHECKING, TypedDict, TypeVar
 
 if sys.version_info >= (3, 11):
     from typing import Self as Self
@@ -22,3 +22,13 @@ DeviceT = TypeVar("DeviceT", bound="Device")
 DeviceCallbackType = Callable[[DeviceT], None]
 
 TelegramCallbackType = Callable[["Telegram"], None]
+
+
+class DPTMainSubDict(TypedDict):
+    """DPT type dictionary in accordance to xknxproject DPTType data."""
+
+    main: int
+    sub: int | None
+
+
+DPTParsable = str | int | DPTMainSubDict


Test patch:
diff --git a/test/dpt_tests/dpt_test.py b/test/dpt_tests/dpt_test.py
index 8d7a343e..3ca86201 100644
--- a/test/dpt_tests/dpt_test.py
+++ b/test/dpt_tests/dpt_test.py
@@ -7,6 +7,8 @@ import pytest
 
 from xknx.dpt import (
     DPT2ByteFloat,
+    DPT2ByteUnsigned,
+    DPTActiveEnergy,
     DPTArray,
     DPTBase,
     DPTBinary,
@@ -109,6 +111,59 @@ class TestDPTBase:
         assert issubclass(parsed[0], DPTBase)
         assert all(parsed[0] == dpt for dpt in parsed)
 
+    @pytest.mark.parametrize(
+        "equal_dpts",
+        [
+            # strings in dictionaries would fail type checking, but should work nevertheless
+            [
+                "2byte_unsigned",
+                7,
+                "DPT-7",
+                {"main": 7},
+                {"main": "7", "sub": None},
+                DPT2ByteUnsigned,
+            ],
+            [
+                "temperature",
+                "9.001",
+                {"main": 9, "sub": 1},
+                {"main": "9", "sub": "1"},
+                DPTTemperature,
+            ],
+            ["active_energy", "13.010", {"main": 13, "sub": 10}, DPTActiveEnergy],
+        ],
+    )
+    def test_get_dpt_alternative_notations(self, equal_dpts: list[Any]) -> None:
+        """Test the parser for accepting alternative notations for the same DPT class."""
+        parsed = [DPTBase.get_dpt(dpt) for dpt in equal_dpts]
+        assert issubclass(parsed[0], DPTBase)
+        assert all(parsed[0] == dpt for dpt in parsed)
+
+    INVALID_DPT_IDENTIFIERS = [
+        None,
+        0,
+        999999999,
+        9.001,  # float is not valid
+        "invalid_string",
+        {"sub": 1},
+        {"main": None, "sub": None},
+        {"main": "invalid"},
+        {"main": 9, "sub": "invalid"},
+        [9, 1],
+        (9,),
+    ]
+
+    @pytest.mark.parametrize("value", INVALID_DPT_IDENTIFIERS)
+    def test_parse_transcoder_invalid_data(self, value: Any) -> None:
+        """Test parsing invalid data."""
+        assert DPTBase.parse_transcoder(value) is None
+
+    @pytest.mark.parametrize("value", INVALID_DPT_IDENTIFIERS)
+    def test_get_dpt_invalid_data(self, value: Any) -> None:
+        """Test parsing invalid data."""
+        with pytest.raises(ValueError):
+            DPTBase.get_dpt(value)
+
     def test_parse_transcoder_from_subclass(self) -> None:
         """Test parsing only subclasses of a DPT class."""
         assert DPTBase.parse_transcoder("string") == DPTString
@@ -123,25 +178,18 @@ class TestDPTBase:
         assert DPTNumeric.parse_transcoder("temperature") == DPTTemperature
         assert DPT2ByteFloat.parse_transcoder("temperature") == DPTTemperature
 
-    @pytest.mark.parametrize(
-        "value",
-        [
-            None,
-            0,
-            999999999,
-            9.001,  # float is not valid
-            "invalid_string",
-            {"sub": 1},
-            {"main": None, "sub": None},
-            {"main": "invalid"},
-            {"main": 9, "sub": "invalid"},
-            [9, 1],
-            (9,),
-        ],
-    )
-    def test_parse_transcoder_invalid_data(self, value: Any) -> None:
-        """Test parsing invalid data."""
-        assert DPTBase.parse_transcoder(value) is None
+    def test_get_dpt_from_subclass(self) -> None:
+        """Test parsing only subclasses of a DPT class."""
+        assert DPTBase.get_dpt("string") == DPTString
+        with pytest.raises(ValueError):
+            DPTNumeric.get_dpt("string")
+
+        assert DPTBase.get_dpt("percent") == DPTScaling
+        assert DPTNumeric.get_dpt("percent") == DPTScaling
+
+        assert DPTBase.get_dpt("temperature") == DPTTemperature
+        assert DPTNumeric.get_dpt("temperature") == DPTTemperature
+        assert DPT2ByteFloat.get_dpt("temperature") == DPTTemperature
 
     def test_dpt_name(self) -> None:
         """Test DPT name."""