Repository: XKNX/xknx
Pull request #711 (2021-06-07 05:33:12)

DPTNumeric
<!--
  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.
-->
Add a common base class for all numeric DPT definitions. 

- `DPTBase.parse_transcoder` is now a classmethod to allow parsing only subclasses.
- Add `DPTNumeric` as base class for DPTs representing numeric values.

This allows to check for DPTNumeric e.g. in ReomteValueSensor by `issubclass(dpt_class, DPTNumeric)` and parsing value_types that guarantee to accept and return `int | float` by `DPTNumeric.parse_transcoder(value_type)`

I want to use this for HAs expose values and for a number entity.

## 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)
- [ ] This change requires a documentation update

## Checklist:

- [x] 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


codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/711?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#711](https://codecov.io/gh/XKNX/xknx/pull/711?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (aa05c2d) into [main](https://codecov.io/gh/XKNX/xknx/commit/c450d5934c8ddc608741229a7d14168013c3684c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (c450d59) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

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

```diff
@@           Coverage Diff           @@
##             main     #711   +/-   ##
=======================================
  Coverage   94.53%   94.54%           
=======================================
  Files         103      103           
  Lines        6554     6564   +10     
=======================================
+ Hits         6196     6206   +10     
  Misses        358      358           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/711?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/dpt/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0LnB5) | `99.12% <100.00%> (+0.10%)` | :arrow_up: |
| [xknx/dpt/dpt\_1byte\_signed.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzFieXRlX3NpZ25lZC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_1byte\_uint.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzFieXRlX3VpbnQucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_2byte\_float.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzJieXRlX2Zsb2F0LnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_2byte\_signed.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzJieXRlX3NpZ25lZC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_2byte\_uint.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzJieXRlX3VpbnQucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_4byte\_float.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzRieXRlX2Zsb2F0LnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_4byte\_int.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzRieXRlX2ludC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_scaling.py](https://codecov.io/gh/XKNX/xknx/pull/711/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0X3NjYWxpbmcucHk=) | `97.67% <100.00%> (ø)` | |


Patch:
diff --git a/changelog.md b/changelog.md
index 36258221..e6725eb6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## Unreleased changes
+
+### Internals
+
+- `DPTBase.parse_transcoder` is now a classmethod to allow parsing only subclasses.
+- Add `DPTNumeric` as base class for DPTs representing numeric values.
+
 ## 0.18.4 ClimateMode bugfix 2021-06-04
 
 ### Bugfix
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index 92d882f8..669c6993 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -5,7 +5,7 @@ Module for encoding and decoding KNX datatypes.
 * Derived KNX Values like Scaling, Temperature
 """
 # flake8: noqa
-from .dpt import DPTArray, DPTBase, DPTBinary
+from .dpt import DPTArray, DPTBase, DPTBinary, DPTNumeric
 from .dpt_1byte_signed import DPTPercentV8, DPTSignedRelativeValue, DPTValue1Count
 from .dpt_1byte_uint import (
     DPTDecimalFactor,
@@ -268,6 +268,7 @@ __all__ = [
     "DPTMassFlux",
     "DPTMol",
     "DPTMomentum",
+    "DPTNumeric",
     "DPTPartsPerMillion",
     "DPTPercentU8",
     "DPTPercentV16",
diff --git a/xknx/dpt/dpt.py b/xknx/dpt/dpt.py
index ba163d69..c7db9888 100644
--- a/xknx/dpt/dpt.py
+++ b/xknx/dpt/dpt.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 from abc import ABC, abstractmethod
+from inspect import isabstract
 from typing import Any, Iterator, TypeVar, cast
 
 from xknx.exceptions import ConversionError
@@ -82,7 +83,8 @@ class DPTBase(ABC):
         """Yield all subclasses and their subclasses."""
         for subclass in cls.__subclasses__():
             yield from subclass.__recursive_subclasses__()
-            yield subclass
+            if not isabstract(subclass):
+                yield subclass
 
     @classmethod
     def has_distinct_dpt_numbers(cls) -> bool:
@@ -94,52 +96,67 @@ class DPTBase(ABC):
         """Return True if value_type is defined (not inherited)."""
         return "value_type" in cls.__dict__
 
-    @staticmethod
+    @classmethod
     def transcoder_by_dpt(
-        dpt_main: int, dpt_sub: int | None = None
+        cls, dpt_main: int, dpt_sub: int | None = None
     ) -> type[DPTBase] | None:
         """Return Class reference of DPTBase subclass with matching DPT number."""
-        for dpt in DPTBase.__recursive_subclasses__():
+        for dpt in cls.__recursive_subclasses__():
             if dpt.has_distinct_dpt_numbers():
                 if dpt_main == dpt.dpt_main_number and dpt_sub == dpt.dpt_sub_number:
                     return dpt
         return None
 
-    @staticmethod
-    def transcoder_by_value_type(value_type: str) -> type[DPTBase] | None:
+    @classmethod
+    def transcoder_by_value_type(cls, value_type: str) -> type[DPTBase] | None:
         """Return Class reference of DPTBase subclass with matching value_type."""
-        for dpt in DPTBase.__recursive_subclasses__():
+        for dpt in cls.__recursive_subclasses__():
             if dpt.has_distinct_value_type():
                 if value_type == dpt.value_type:
                     return dpt
         return None
 
-    # TODO: convert to classmethod to allow parsing only subclasses (eg. for Numeric, Control etc.)
-    @staticmethod
-    def parse_transcoder(value_type: int | str) -> type[DPTBase] | None:
+    @classmethod
+    def parse_transcoder(cls, value_type: int | str) -> type[DPTBase] | None:
         """Return Class reference of DPTBase subclass from value_type or DPT number."""
         if isinstance(value_type, int):
-            return DPTBase.transcoder_by_dpt(value_type)
+            return cls.transcoder_by_dpt(value_type)
         if isinstance(value_type, str):
             string_type = value_type.strip()
-            transcoder = DPTBase.transcoder_by_value_type(string_type)
+            transcoder = cls.transcoder_by_value_type(string_type)
             if transcoder is None:
-                # Try to parse the value_type if it is a string but not found by DPTBase.transcoder_by_value_type()
+                # Try to parse the value_type if it is a string but not found by cls.transcoder_by_value_type()
                 # for backwards compatibility (eg. "DPT-5") and strings representing numbers (eg. "7", "9.001")
                 string_type = string_type.upper().strip(" DPT-")
                 if string_type.isdigit():
-                    transcoder = DPTBase.transcoder_by_dpt(int(string_type))
+                    transcoder = cls.transcoder_by_dpt(int(string_type))
                 else:
                     try:
                         main, sub = map(int, string_type.split("."))
-                        transcoder = DPTBase.transcoder_by_dpt(
-                            dpt_main=main, dpt_sub=sub
-                        )
+                        transcoder = cls.transcoder_by_dpt(dpt_main=main, dpt_sub=sub)
                     except (ValueError, IndexError):
                         pass
             return transcoder
 
 
+class DPTNumeric(DPTBase):
+    """Base class for KNX data point types decoding numeric values."""
+
+    value_min: int | float
+    value_max: int | float
+    resolution: int | float
+
+    @classmethod
+    @abstractmethod
+    def from_knx(cls, raw: tuple[int, ...]) -> int | float:
+        """Parse/deserialize from KNX/IP raw data (big endian)."""
+
+    @classmethod
+    @abstractmethod
+    def to_knx(cls, value: int | float) -> bytes | tuple[int, ...]:
+        """Serialize to KNX/IP raw data."""
+
+
 class DPTBinary:
     """The DPTBinary is a base class for all datatypes encoded directly into the last 6 bit of the APCI (mostly integer)."""
 
diff --git a/xknx/dpt/dpt_1byte_signed.py b/xknx/dpt/dpt_1byte_signed.py
index 8e0fce94..ecdc26fd 100644
--- a/xknx/dpt/dpt_1byte_signed.py
+++ b/xknx/dpt/dpt_1byte_signed.py
@@ -3,24 +3,26 @@ from __future__ import annotations
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPTSignedRelativeValue(DPTBase):
+class DPTSignedRelativeValue(DPTNumeric):
     """
     Abstraction for KNX 1 Byte "1-octet Signed Relative Value".
 
     DPT 6.***
     """
 
-    value_min = -128
-    value_max = 127
     dpt_main_number = 6
     dpt_sub_number: int | None = None
     value_type = "1byte_signed"
     unit = ""
     payload_length = 1
 
+    value_min = -128
+    value_max = 127
+    resolution = 1
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> int:
         """Parse/deserialize from KNX/IP raw data."""
@@ -30,7 +32,7 @@ class DPTSignedRelativeValue(DPTBase):
         return raw[0]
 
     @classmethod
-    def to_knx(cls, value: int) -> tuple[int]:
+    def to_knx(cls, value: int | float) -> tuple[int]:
         """Serialize to KNX/IP raw data."""
         try:
             knx_value = int(value)
diff --git a/xknx/dpt/dpt_1byte_uint.py b/xknx/dpt/dpt_1byte_uint.py
index 18c7e9b4..4e76fadc 100644
--- a/xknx/dpt/dpt_1byte_uint.py
+++ b/xknx/dpt/dpt_1byte_uint.py
@@ -3,25 +3,26 @@ from __future__ import annotations
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPTValue1ByteUnsigned(DPTBase):
+class DPTValue1ByteUnsigned(DPTNumeric):
     """
     Abstraction for KNX 1 Octet.
 
     DPT 5.***
     """
 
-    value_min = 0
-    value_max = 255
     dpt_main_number = 5
     dpt_sub_number: int | None = None
     value_type = "1byte_unsigned"
     unit = ""
-    resolution = 1
     payload_length = 1
 
+    value_min = 0
+    value_max = 255
+    resolution = 1
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> int:
         """Parse/deserialize from KNX/IP raw data."""
@@ -37,7 +38,7 @@ class DPTValue1ByteUnsigned(DPTBase):
         return value
 
     @classmethod
-    def to_knx(cls, value: int) -> tuple[int]:
+    def to_knx(cls, value: int | float) -> tuple[int]:
         """Serialize to KNX/IP raw data."""
         try:
             knx_value = int(value)
@@ -111,13 +112,14 @@ class DPTSceneNumber(DPTValue1ByteUnsigned):
     DPT 17.001
     """
 
-    value_min = 1
-    value_max = 64
     dpt_main_number = 17
     dpt_sub_number = 1
     value_type = "scene_number"
     unit = ""
 
+    value_min = 1
+    value_max = 64
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> int:
         """Parse/deserialize from KNX/IP raw data."""
@@ -133,7 +135,7 @@ class DPTSceneNumber(DPTValue1ByteUnsigned):
         return value
 
     @classmethod
-    def to_knx(cls, value: int) -> tuple[int]:
+    def to_knx(cls, value: int | float) -> tuple[int]:
         """Serialize to KNX/IP raw data."""
         try:
             knx_value = int(value) - 1
diff --git a/xknx/dpt/dpt_2byte_float.py b/xknx/dpt/dpt_2byte_float.py
index cdf2d97d..7d8090d1 100644
--- a/xknx/dpt/dpt_2byte_float.py
+++ b/xknx/dpt/dpt_2byte_float.py
@@ -7,25 +7,26 @@ from __future__ import annotations
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPT2ByteFloat(DPTBase):
+class DPT2ByteFloat(DPTNumeric):
     """
     Abstraction for KNX 2 Octet Floating Point Numbers.
 
     DPT 9.***
     """
 
-    value_min = -671088.64
-    value_max = 670760.96
     dpt_main_number = 9
     dpt_sub_number: int | None = None
     value_type = "2byte_float"
     unit = ""
-    resolution = 0.01
     payload_length = 2
 
+    value_min = -671088.64
+    value_max = 670760.96
+    resolution = 0.01
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> float:
         """Parse/deserialize from KNX/IP raw data."""
@@ -87,84 +88,91 @@ class DPT2ByteFloat(DPTBase):
 class DPTTemperature(DPT2ByteFloat):
     """DPT 9.001 DPT_Value_Temp."""
 
-    value_min = -273
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 1
     value_type = "temperature"
     unit = "°C"
     ha_device_class = "temperature"
 
+    value_min = -273
+    value_max = 670760
+
 
 class DPTTemperatureDifference2Byte(DPT2ByteFloat):
     """DPT 9.002 DPT_Value_Tempd."""
 
-    value_min = -670760
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 2
     value_type = "temperature_difference_2byte"
     unit = "K"
     ha_device_class = "temperature"
 
+    value_min = -670760
+    value_max = 670760
+
 
 class DPTTemperatureA(DPT2ByteFloat):
     """DPT 9.003 DPT_Value_Tempa."""
 
-    value_min = -670760
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 3
     value_type = "temperature_a"
     unit = "K/h"
 
+    value_min = -670760
+    value_max = 670760
+
 
 class DPTLux(DPT2ByteFloat):
     """DPT 9.004 DPT_Value_Lux."""
 
-    value_min = 0
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 4
     value_type = "illuminance"
     unit = "lx"
     ha_device_class = "illuminance"
 
+    value_min = 0
+    value_max = 670760
+
 
 class DPTWsp(DPT2ByteFloat):
     """DPT 9.005 DPT_Value_Ws Speed (m/s)."""
 
-    value_min = 0
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 5
     value_type = "wind_speed_ms"
     unit = "m/s"
 
+    value_min = 0
+    value_max = 670760
+
 
 class DPTPressure2Byte(DPT2ByteFloat):
     """DPT 9.006 DPT_Value_Pres (Pa)."""
 
-    value_min = 0
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 6
     value_type = "pressure_2byte"
     unit = "Pa"
     ha_device_class = "pressure"
 
+    value_min = 0
+    value_max = 670760
+
 
 class DPTHumidity(DPT2ByteFloat):
     """DPT 9.007 DPT_Value_Humidity."""
 
-    value_min = 0
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 7
     value_type = "humidity"
     unit = "%"
     ha_device_class = "humidity"
 
+    value_min = 0
+    value_max = 670760
+
 
 class DPTPartsPerMillion(DPT2ByteFloat):
     """DPT 9.008 DPT_Value_parts/million."""
@@ -178,24 +186,26 @@ class DPTPartsPerMillion(DPT2ByteFloat):
 class DPTTime1(DPT2ByteFloat):
     """DPT 9.010 DPT_Value_Time1 (s)."""
 
-    value_min = -670760
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 10
     value_type = "time_1"
     unit = "s"
 
+    value_min = -670760
+    value_max = 670760
+
 
 class DPTTime2(DPT2ByteFloat):
     """DPT 9.011 DPT_Value_Time2 (ms)."""
 
-    value_min = -670760
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 11
     value_type = "time_2"
     unit = "ms"
 
+    value_min = -670760
+    value_max = 670760
+
 
 class DPTVoltage(DPT2ByteFloat):
     """DPT 9.020 DPT_Value_Voltage."""
@@ -255,36 +265,39 @@ class DPTVolumeFlow(DPT2ByteFloat):
 class DPTRainAmount(DPT2ByteFloat):
     """DPT 9.026 DPT_Rain_Amount (l/m²)."""
 
-    value_min = -671088.64
-    value_max = 670760.96
     dpt_main_number = 9
     dpt_sub_number = 26
     value_type = "rain_amount"
     unit = "l/m²"
 
+    value_min = -671088.64
+    value_max = 670760.96
+
 
 class DPTTemperatureF(DPT2ByteFloat):
     """DPT 9.027 DPT_Value_Temp_F."""
 
-    value_min = -459.6
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 27
     value_type = "temperature_f"
     unit = "°F"
     ha_device_class = "temperature"
 
+    value_min = -459.6
+    value_max = 670760
+
 
 class DPTWspKmh(DPT2ByteFloat):
     """DPT 9.028 DPT_Value_Wsp_kmh Speed (km/h)."""
 
-    value_min = 0
-    value_max = 670760
     dpt_main_number = 9
     dpt_sub_number = 28
     value_type = "wind_speed_kmh"
     unit = "km/h"
 
+    value_min = 0
+    value_max = 670760
+
 
 class DPTEnthalpy(DPT2ByteFloat):
     """DPT 9.? 2-byte float value (with unit)."""
diff --git a/xknx/dpt/dpt_2byte_signed.py b/xknx/dpt/dpt_2byte_signed.py
index 0585e840..b1b9de5d 100644
--- a/xknx/dpt/dpt_2byte_signed.py
+++ b/xknx/dpt/dpt_2byte_signed.py
@@ -10,25 +10,26 @@ import struct
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPT2ByteSigned(DPTBase):
+class DPT2ByteSigned(DPTNumeric):
     """
     Abstraction for KNX 2 Byte signed values.
 
     DPT 8.***
     """
 
-    value_min = -32768
-    value_max = 32767
     dpt_main_number = 8
     dpt_sub_number: int | None = None
     value_type = "2byte_signed"
     unit = ""
-    resolution: float = 1
     payload_length = 2
 
+    value_min = -32768
+    value_max = 32767
+    resolution = 1
+
     _struct_format = ">h"
 
     @classmethod
@@ -42,7 +43,7 @@ class DPT2ByteSigned(DPTBase):
             raise ConversionError("Could not parse %s" % cls.__name__, raw=raw)
 
     @classmethod
-    def to_knx(cls, value: int) -> tuple[int, ...]:
+    def to_knx(cls, value: int | float) -> tuple[int, ...]:
         """Serialize to KNX/IP raw data."""
         try:
             knx_value = int(value)
@@ -83,7 +84,6 @@ class DPTDeltaTime10Msec(DPT2ByteSigned):
     dpt_sub_number = 3
     value_type = "delta_time_10ms"
     unit = "ms"
-    resolution = 10
 
 
 class DPTDeltaTime100Msec(DPT2ByteSigned):
@@ -93,7 +93,6 @@ class DPTDeltaTime100Msec(DPT2ByteSigned):
     dpt_sub_number = 4
     value_type = "delta_time_100ms"
     unit = "ms"
-    resolution = 100
 
 
 class DPTDeltaTimeSec(DPT2ByteSigned):
@@ -130,7 +129,6 @@ class DPTPercentV16(DPT2ByteSigned):
     dpt_sub_number = 10
     value_type = "percentV16"
     unit = "%"
-    resolution = 0.01
 
 
 class DPTRotationAngle(DPT2ByteSigned):
diff --git a/xknx/dpt/dpt_2byte_uint.py b/xknx/dpt/dpt_2byte_uint.py
index e13cd047..3764ba9f 100644
--- a/xknx/dpt/dpt_2byte_uint.py
+++ b/xknx/dpt/dpt_2byte_uint.py
@@ -3,10 +3,10 @@ from __future__ import annotations
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPT2ByteUnsigned(DPTBase):
+class DPT2ByteUnsigned(DPTNumeric):
     """
     Abstraction for KNX 2 Byte "2-octet unsigned value".
 
@@ -15,15 +15,16 @@ class DPT2ByteUnsigned(DPTBase):
     DPT 7.***
     """
 
-    value_min = 0
-    value_max = 65535
     dpt_main_number = 7
     dpt_sub_number: int | None = None
     value_type = "2byte_unsigned"
     unit = ""
-    resolution = 1
     payload_length = 2
 
+    value_min = 0
+    value_max = 65535
+    resolution = 1
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> int:
         """Parse/deserialize from KNX/IP raw data."""
@@ -31,7 +32,7 @@ class DPT2ByteUnsigned(DPTBase):
         return (raw[0] * 256) + raw[1]
 
     @classmethod
-    def to_knx(cls, value: int) -> tuple[int, int]:
+    def to_knx(cls, value: int | float) -> tuple[int, int]:
         """Serialize to KNX/IP raw data."""
         try:
             knx_value = int(value)
@@ -72,7 +73,6 @@ class DPTTimePeriod10Msec(DPT2ByteUnsigned):
     dpt_sub_number = 3
     value_type = "time_period_10msec"
     unit = "ms"
-    resolution = 10
 
 
 class DPTTimePeriod100Msec(DPT2ByteUnsigned):
@@ -82,7 +82,6 @@ class DPTTimePeriod100Msec(DPT2ByteUnsigned):
     dpt_sub_number = 4
     value_type = "time_period_100msec"
     unit = "ms"
-    resolution = 100
 
 
 class DPTTimePeriodSec(DPT2ByteUnsigned):
diff --git a/xknx/dpt/dpt_4byte_float.py b/xknx/dpt/dpt_4byte_float.py
index d013fc36..d1c69b8f 100644
--- a/xknx/dpt/dpt_4byte_float.py
+++ b/xknx/dpt/dpt_4byte_float.py
@@ -11,10 +11,10 @@ from typing import cast
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPT4ByteFloat(DPTBase):
+class DPT4ByteFloat(DPTNumeric):
     """
     Abstraction for KNX 4 Octet Floating Point Numbers, with a maximum usable range as specified in IEEE 754.
 
@@ -32,6 +32,10 @@ class DPT4ByteFloat(DPTBase):
     unit = ""
     payload_length = 4
 
+    value_min = float("-inf")
+    value_max = float("inf")
+    resolution = 0.0000001
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> float:
         """Parse/deserialize from KNX/IP raw data (big endian)."""
diff --git a/xknx/dpt/dpt_4byte_int.py b/xknx/dpt/dpt_4byte_int.py
index 73bae9af..872688e9 100644
--- a/xknx/dpt/dpt_4byte_int.py
+++ b/xknx/dpt/dpt_4byte_int.py
@@ -11,25 +11,26 @@ import struct
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPT4ByteUnsigned(DPTBase):
+class DPT4ByteUnsigned(DPTNumeric):
     """
     Abstraction for KNX 4 Byte "32-bit unsigned".
 
     DPT 12.***
     """
 
-    value_min = 0
-    value_max = 4294967295
     dpt_main_number = 12
     dpt_sub_number: int | None = None
     value_type = "4byte_unsigned"
     unit = ""
-    resolution: float = 1
     payload_length = 4
 
+    value_min = 0
+    value_max = 4294967295
+    resolution = 1
+
     _struct_format = ">I"
 
     @classmethod
@@ -43,7 +44,7 @@ class DPT4ByteUnsigned(DPTBase):
             raise ConversionError("Could not parse %s" % cls.__name__, raw=raw)
 
     @classmethod
-    def to_knx(cls, value: int) -> tuple[int, ...]:
+    def to_knx(cls, value: int | float) -> tuple[int, ...]:
         """Serialize to KNX/IP raw data."""
         try:
             knx_value = int(value)
@@ -66,13 +67,14 @@ class DPT4ByteSigned(DPT4ByteUnsigned):
     DPT 13.***
     """
 
-    value_min = -2147483648
-    value_max = 2147483647
     dpt_main_number = 13
     dpt_sub_number: int | None = None
     value_type = "4byte_signed"
     unit = ""
-    resolution: float = 1
+
+    value_min = -2147483648
+    value_max = 2147483647
+    resolution = 1
 
     _struct_format = ">i"
 
@@ -111,7 +113,6 @@ class DPTFlowRateM3H(DPT4ByteSigned):
     dpt_sub_number = 2
     value_type = "flow_rate_m3h"
     unit = "m³/h"
-    resolution = 0.0001
 
 
 class DPTActiveEnergy(DPT4ByteSigned):
diff --git a/xknx/dpt/dpt_scaling.py b/xknx/dpt/dpt_scaling.py
index d2f73fc9..92ac3ec3 100644
--- a/xknx/dpt/dpt_scaling.py
+++ b/xknx/dpt/dpt_scaling.py
@@ -3,25 +3,26 @@ from __future__ import annotations
 
 from xknx.exceptions import ConversionError
 
-from .dpt import DPTBase
+from .dpt import DPTNumeric
 
 
-class DPTScaling(DPTBase):
+class DPTScaling(DPTNumeric):
     """
     Abstraction for KNX 1 Octet Percent.
 
     DPT 5.001
     """
 
-    value_min = 0
-    value_max = 100
-    resolution = 100 / 255
     dpt_main_number = 5
     dpt_sub_number = 1
     value_type = "percent"
     unit = "%"
     payload_length = 1
 
+    value_min = 0
+    value_max = 100
+    resolution = 1
+
     @classmethod
     def from_knx(cls, raw: tuple[int, ...]) -> int:
         """Parse/deserialize from KNX/IP raw data."""
@@ -65,10 +66,11 @@ class DPTAngle(DPTScaling):
     DPT 5.003
     """
 
-    value_min = 0
-    value_max = 360
-    resolution = 360 / 255
     dpt_main_number = 5
     dpt_sub_number = 3
     value_type = "angle"
     unit = "°"
+
+    value_min = 0
+    value_max = 360
+    resolution = 1


Test patch:
diff --git a/test/dpt_tests/dpt_test.py b/test/dpt_tests/dpt_test.py
index cbe8997f..2cfb8e68 100644
--- a/test/dpt_tests/dpt_test.py
+++ b/test/dpt_tests/dpt_test.py
@@ -1,6 +1,15 @@
 """Unit test for KNX binary/integer objects."""
 import pytest
-from xknx.dpt import DPTArray, DPTBase, DPTBinary
+from xknx.dpt import (
+    DPT2ByteFloat,
+    DPTArray,
+    DPTBase,
+    DPTBinary,
+    DPTNumeric,
+    DPTScaling,
+    DPTString,
+    DPTTemperature,
+)
 from xknx.exceptions import ConversionError
 
 
@@ -78,6 +87,11 @@ class TestDPT:
 class TestDPTBase:
     """Test class for transcoder base object."""
 
+    def test_dpt_abstract_subclasses_ignored(self):
+        """Test if abstract base classes are ignored by __recursive_subclasses__."""
+        for dpt in DPTBase.__recursive_subclasses__():
+            assert dpt is not DPTNumeric
+
     def test_dpt_subclasses_definition_types(self):
         """Test value_type and dpt_*_number values for correct type in subclasses of DPTBase."""
         for dpt in DPTBase.__recursive_subclasses__():
@@ -128,3 +142,29 @@ class TestDPTBase:
         dpt7 = DPTBase.parse_transcoder("active_energy")
         dpt8 = DPTBase.parse_transcoder("13.010")
         assert dpt7 == dpt8
+
+    def test_parse_transcoder_from_subclass(self):
+        """Test parsing only subclasses of a DPT class."""
+        assert DPTBase.parse_transcoder("string") == DPTString
+        assert DPTNumeric.parse_transcoder("string") is None
+        assert DPT2ByteFloat.parse_transcoder("string") is None
+
+        assert DPTBase.parse_transcoder("percent") == DPTScaling
+        assert DPTNumeric.parse_transcoder("percent") == DPTScaling
+        assert DPT2ByteFloat.parse_transcoder("percent") is None
+
+        assert DPTBase.parse_transcoder("temperature") == DPTTemperature
+        assert DPTNumeric.parse_transcoder("temperature") == DPTTemperature
+        assert DPT2ByteFloat.parse_transcoder("temperature") == DPTTemperature
+
+
+class TestDPTNumeric:
+    """Test class for numeric transcoder base object."""
+
+    @pytest.mark.parametrize("dpt_class", DPTNumeric.__recursive_subclasses__())
+    def test_values(self, dpt_class):
+        """Test boundary values are set for numeric definitions (because mypy doesn't)."""
+
+        assert isinstance(dpt_class.value_min, (int, float))
+        assert isinstance(dpt_class.value_max, (int, float))
+        assert isinstance(dpt_class.resolution, (int, float))