Repository: XKNX/xknx
Pull request #712 (2021-06-08 06:39:01)

Bump astroid from 2.5.7 to 2.5.8
Bumps [astroid](https://github.com/PyCQA/astroid) from 2.5.7 to 2.5.8.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/PyCQA/astroid/blob/master/ChangeLog">astroid's changelog</a>.</em></p>
<blockquote>
<h1>What's New in astroid 2.5.8?</h1>
<p>Release Date: 2021-06-07</p>
<ul>
<li>
<p>Improve support for Pattern Matching</p>
</li>
<li>
<p>Add lineno and col_offset for <code>Keyword</code> nodes and Python 3.9+</p>
</li>
<li>
<p>Add global inference cache to speed up inference of long statement blocks</p>
</li>
<li>
<p>Add a limit to the total number of nodes inferred indirectly as a result
of inferring some node</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/PyCQA/astroid/commit/1342591e2beb955a377e4486e5595478f79789e8"><code>1342591</code></a> Prepare for 2.5.8 release (<a href="https://github-redirect.dependabot.com/PyCQA/astroid/issues/1013">#1013</a>)</li>
<li><a href="https://github.com/PyCQA/astroid/commit/cf6528cbc158097c4903f0cab68242ff14bb591b"><code>cf6528c</code></a> Performance improvements to counter context.clone slowdown (<a href="https://github-redirect.dependabot.com/PyCQA/astroid/issues/1009">#1009</a>)</li>
<li><a href="https://github.com/PyCQA/astroid/commit/85791635a24cd300434ab2fa31acf21835315227"><code>8579163</code></a> Add lineno and col_offset to Keyword nodes (<a href="https://github-redirect.dependabot.com/PyCQA/astroid/issues/1011">#1011</a>)</li>
<li><a href="https://github.com/PyCQA/astroid/commit/6e8a5756c6877977f841f00bc2c47720913a7e41"><code>6e8a575</code></a> Improve support for Pattern Matching (<a href="https://github-redirect.dependabot.com/PyCQA/astroid/issues/1010">#1010</a>)</li>
<li><a href="https://github.com/PyCQA/astroid/commit/cc1968a19b8b1e1c4e864021ae3964d73e5b3aff"><code>cc1968a</code></a> Bump pyupgrade to v2.19.1 (<a href="https://github-redirect.dependabot.com/PyCQA/astroid/issues/1012">#1012</a>)</li>
<li><a href="https://github.com/PyCQA/astroid/commit/d3fea8fee322200884920620cd9f9cb339ee9158"><code>d3fea8f</code></a> [pre-commit.ci] pre-commit autoupdate (<a href="https://github-redirect.dependabot.com/PyCQA/astroid/issues/1005">#1005</a>)</li>
<li><a href="https://github.com/PyCQA/astroid/commit/8f70c7dade73817a19b686bbb463b1aba0c7397a"><code>8f70c7d</code></a> Post release chores</li>
<li>See full diff in <a href="https://github.com/PyCQA/astroid/compare/v2.5.7...v2.5.8">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astroid&package-manager=pip&previous-version=2.5.7&new-version=2.5.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/712?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#712](https://codecov.io/gh/XKNX/xknx/pull/712?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (0896322) 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 **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@           Coverage Diff           @@
##             main     #712   +/-   ##
=======================================
  Coverage   94.53%   94.53%           
=======================================
  Files         103      103           
  Lines        6554     6554           
=======================================
  Hits         6196     6196           
  Misses        358      358           
```




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/requirements/testing.txt b/requirements/testing.txt
index e3c3d455..0e2398d0 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -4,7 +4,7 @@ isort==5.8.0
 flake8==3.9.2
 flake8-isort==4.0.0
 pydocstyle==6.1.1
-astroid==2.5.7
+astroid==2.5.8
 pylint==2.7.4
 pytest==6.2.4
 pytest-asyncio==0.15.1
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))