Repository: XKNX/xknx
Pull request #1655 (2025-02-10 09:22:26)

Add `DPTBase.dpt_number_str` and `DPTBase.dpt_name` classmethods
<!--
  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 `DPTBase.dpt_number_str` and `DPTBase.dpt_name` classmethods for human readable DPT number (eg. "9.001") and class name (eg. "DPTTemperature (9.001)").

Fixes # (issue)

cc @philippwaller

## 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)
- [ ] 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/1655?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.92%. Comparing base [(`6e28574`)](https://app.codecov.io/gh/XKNX/xknx/commit/6e285740674c14b3e91fdb5cd4b12262d82ce994?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`4ca9409`)](https://app.codecov.io/gh/XKNX/xknx/commit/4ca940933847a166e5b9092514c5e1dfe22f39da?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/1655/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/1655?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1655   +/-   ##
=======================================
  Coverage   96.92%   96.92%           
=======================================
  Files         160      160           
  Lines       10702    10712   +10     
=======================================
+ Hits        10373    10383   +10     
  Misses        329      329           
```

| [Files with missing lines](https://app.codecov.io/gh/XKNX/xknx/pull/1655?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/dpt/dpt.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?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/dpt/dpt\_10.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_10.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzEwLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_11.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_11.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzExLnB5) | `100.00% <ø> (ø)` | |
| [xknx/dpt/dpt\_14.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_14.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzE0LnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_16.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_16.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzE2LnB5) | `100.00% <ø> (ø)` | |
| [xknx/dpt/dpt\_17.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_17.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzE3LnB5) | `100.00% <ø> (ø)` | |
| [xknx/dpt/dpt\_19.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_19.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzE5LnB5) | `98.30% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_5.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_5.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzUucHk=) | `100.00% <ø> (ø)` | |
| [xknx/dpt/dpt\_6.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_6.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzYucHk=) | `100.00% <ø> (ø)` | |
| [xknx/dpt/dpt\_7.py](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_7.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzcucHk=) | `100.00% <ø> (ø)` | |
| ... and [2 more](https://app.codecov.io/gh/XKNX/xknx/pull/1655?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | |

</details>

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 4311b82c..e83aa5d8 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,10 @@ nav_order: 2
 
 # Changelog
 
+### 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)").
+
 # 3.5.0 Swing it 2025-01-28
 
 ### Devices
diff --git a/xknx/core/group_address_dpt.py b/xknx/core/group_address_dpt.py
index e2860ed5..72d0bd1d 100644
--- a/xknx/core/group_address_dpt.py
+++ b/xknx/core/group_address_dpt.py
@@ -71,12 +71,12 @@ class GroupAddressDPT:
         try:
             value = transcoder.from_knx(telegram.payload.value)
         except (CouldNotParseTelegram, ConversionError) as err:
-            _GA_DPT_LOGGER.warning(
+            _GA_DPT_LOGGER.debug(
                 "DPT decoding error. Telegram from %s to %s with payload %s can't be decoded by %s: %s",
                 telegram.source_address,
                 telegram.destination_address,
                 telegram.payload.value,
-                transcoder.__name__,
+                transcoder.dpt_name(),
                 err,
             )
             return
diff --git a/xknx/dpt/dpt.py b/xknx/dpt/dpt.py
index ebe62f5d..8b1fb34a 100644
--- a/xknx/dpt/dpt.py
+++ b/xknx/dpt/dpt.py
@@ -68,6 +68,20 @@ class DPTBase(ABC):
     unit: str | None = None
     ha_device_class: str | None = None
 
+    @classmethod
+    def dpt_number_str(cls) -> str:
+        """Return DPT number string representation."""
+        if cls.dpt_sub_number is not None:
+            return f"{cls.dpt_main_number}.{cls.dpt_sub_number:03d}"
+        return f"{cls.dpt_main_number or ''}"
+
+    @classmethod
+    def dpt_name(cls) -> str:
+        """Return string representation of class name and DPT number."""
+        if cls.dpt_main_number is not None:
+            return f"{cls.__name__} ({cls.dpt_number_str()})"
+        return f"{cls.__name__} (abstract)"  # concrete classes have dpt_main_number
+
     @classmethod
     @abstractmethod
     def from_knx(cls, payload: DPTArray | DPTBinary) -> Any:
@@ -92,7 +106,7 @@ class DPTBase(ABC):
                 return payload.value
 
             raise CouldNotParseTelegram(
-                f"Invalid payload length for {cls.__name__}",
+                f"Invalid payload length for {cls.dpt_name()}",
                 payload=payload,
                 expected_length=cls.payload_length,
             )
@@ -101,7 +115,7 @@ class DPTBase(ABC):
             if payload.value >= 2**cls.payload_length:
                 # >= 0 is already checked by DPTBinary
                 raise CouldNotParseTelegram(
-                    f"Invalid payload bitlength for {cls.__name__}",
+                    f"Invalid payload bitlength for {cls.dpt_name()}",
                     payload=payload,
                     expected_length=cls.payload_length,
                 )
@@ -109,7 +123,7 @@ class DPTBase(ABC):
             return (payload.value,)
 
         raise CouldNotParseTelegram(
-            f"Invalid payload type for {cls.__name__}",
+            f"Invalid payload type for {cls.dpt_name()}",
             payload=payload,
             expected_type=cls.payload_type.__name__,
         )
@@ -127,9 +141,9 @@ class DPTBase(ABC):
     def __recursive_subclasses__(cls: type[Self]) -> Iterator[type[Self]]:
         """Yield all subclasses and their subclasses."""
         for subclass in cls.__subclasses__():
-            yield from subclass.__recursive_subclasses__()
             if not isabstract(subclass):
                 yield subclass
+            yield from subclass.__recursive_subclasses__()
 
     @classmethod
     def dpt_class_tree(cls: type[Self]) -> Iterator[type[Self]]:
@@ -250,7 +264,7 @@ class DPTStructIntMixin:
         try:
             return struct.unpack(cls._struct_format, bytes(raw))[0]  # type: ignore[no-any-return]
         except struct.error as err:
-            raise ConversionError(f"Could not parse {cls.__name__}", raw=raw) from err
+            raise ConversionError(f"Could not parse {cls.dpt_name()}", raw=raw) from err  # type: ignore[attr-defined]
 
     @classmethod
     def to_knx(cls, value: int | float) -> DPTArray:
@@ -262,7 +276,8 @@ class DPTStructIntMixin:
             return DPTArray(struct.pack(cls._struct_format, knx_value))
         except (ValueError, struct.error) as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}",  # type: ignore[attr-defined]
+                value=value,
             ) from err
 
 
@@ -311,7 +326,7 @@ class DPTEnum(DPTBase, Generic[EnumDataT]):
             return cls.data_type(raw[0])  # type: ignore[no-any-return]
         except ValueError:
             raise ConversionError(
-                f"Payload not supported for {cls.__name__}", raw=raw
+                f"Payload not supported for {cls.dpt_name()}", raw=raw
             ) from None
 
     @classmethod
@@ -321,7 +336,7 @@ class DPTEnum(DPTBase, Generic[EnumDataT]):
             return cls._to_knx(cls.data_type.parse(value))
         except ValueError as err:
             raise ConversionError(
-                f"Value not supported for {cls.data_type.__name__} in {cls.__name__}",
+                f"Value not supported for {cls.data_type.__name__} in {cls.dpt_name()}",
                 value=value,
                 valid_values=cls.get_valid_values(),
             ) from err
@@ -377,7 +392,7 @@ class DPTComplex(DPTBase, Generic[_ComplexDataT]):
             return cls._to_knx(cls.data_type.from_dict(value))
         except (ValueError, TypeError, AttributeError, ConversionError) as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}: {err}", value=value
+                f"Could not serialize {cls.dpt_name()}: {err}", value=value
             ) from err
 
     @classmethod
diff --git a/xknx/dpt/dpt_10.py b/xknx/dpt/dpt_10.py
index b01e1e0e..6ab778c5 100644
--- a/xknx/dpt/dpt_10.py
+++ b/xknx/dpt/dpt_10.py
@@ -92,7 +92,7 @@ class DPTTime(DPTComplex[KNXTime]):
         try:
             DPTTime._test_range(hours, minutes, seconds)
         except ValueError as err:
-            raise ConversionError(f"Could not parse {cls.__name__}: {err}") from err
+            raise ConversionError(f"Could not parse {cls.dpt_name()}: {err}") from err
         return KNXTime(
             hour=hours, minutes=minutes, seconds=seconds, day=KNXDay(weekday)
         )
@@ -103,7 +103,9 @@ class DPTTime(DPTComplex[KNXTime]):
         try:
             DPTTime._test_range(value.hour, value.minutes, value.seconds)
         except ValueError as err:
-            raise ConversionError(f"Could not serialize {cls.__name__}: {err}") from err
+            raise ConversionError(
+                f"Could not serialize {cls.dpt_name()}: {err}"
+            ) from err
         return DPTArray(
             (
                 value.day.value << 5 | value.hour,
diff --git a/xknx/dpt/dpt_11.py b/xknx/dpt/dpt_11.py
index 8a6598d8..1e90b9f3 100644
--- a/xknx/dpt/dpt_11.py
+++ b/xknx/dpt/dpt_11.py
@@ -88,7 +88,7 @@ class DPTDate(DPTComplex[KNXDate]):
             if 1990 <= year < 2000:
                 return year - 1900
             raise ConversionError(
-                f"Could not serialize {cls.__name__}. Year out of range 1990..2089",
+                f"Could not serialize {cls.dpt_name()}. Year out of range 1990..2089",
                 year=year,
             )
 
@@ -96,7 +96,7 @@ class DPTDate(DPTComplex[KNXDate]):
 
         if not DPTDate._test_range(value.day, value.month, knx_year):
             raise ConversionError(
-                f"Could not serialize {cls.__name__}. Value out of range", value=value
+                f"Could not serialize {cls.dpt_name()}. Value out of range", value=value
             )
 
         return DPTArray(
diff --git a/xknx/dpt/dpt_14.py b/xknx/dpt/dpt_14.py
index 5315fd3e..c2041f52 100644
--- a/xknx/dpt/dpt_14.py
+++ b/xknx/dpt/dpt_14.py
@@ -44,7 +44,7 @@ class DPT4ByteFloat(DPTNumeric):
         try:
             raw_float = cast(float, struct.unpack(">f", bytes(raw))[0])
         except struct.error as err:
-            raise ConversionError(f"Could not parse {cls.__name__}", raw=raw) from err
+            raise ConversionError(f"Could not parse {cls.dpt_name()}", raw=raw) from err
         try:
             # round to 7 digit precision independent of exponent - same value as ETS 5.7 group monitor
             return round(raw_float, 7 - ceil(log10(abs(raw_float))))
@@ -62,7 +62,7 @@ class DPT4ByteFloat(DPTNumeric):
             return DPTArray(struct.pack(">f", knx_value))
         except (ValueError, struct.error) as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
 
diff --git a/xknx/dpt/dpt_16.py b/xknx/dpt/dpt_16.py
index ca3afa3d..a362502f 100644
--- a/xknx/dpt/dpt_16.py
+++ b/xknx/dpt/dpt_16.py
@@ -41,7 +41,7 @@ class DPTString(DPTBase):
                 raise ValueError("Value out of range")
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
         # replace invalid characters with question marks
         raw_bytes = knx_value.encode(cls._encoding, errors="replace")
diff --git a/xknx/dpt/dpt_17.py b/xknx/dpt/dpt_17.py
index 62dd87ce..de25ea00 100644
--- a/xknx/dpt/dpt_17.py
+++ b/xknx/dpt/dpt_17.py
@@ -29,7 +29,7 @@ class DPTSceneNumber(DPTValue1ByteUnsigned):
 
         if not cls._test_boundaries(value):
             raise ConversionError(
-                f"Could not parse {cls.__name__}", value=value, payload=payload
+                f"Could not parse {cls.dpt_name()}", value=value, payload=payload
             )
 
         return value
@@ -44,5 +44,5 @@ class DPTSceneNumber(DPTValue1ByteUnsigned):
             return DPTArray(knx_value)
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
diff --git a/xknx/dpt/dpt_19.py b/xknx/dpt/dpt_19.py
index 7dee6117..17a273ea 100644
--- a/xknx/dpt/dpt_19.py
+++ b/xknx/dpt/dpt_19.py
@@ -150,7 +150,7 @@ class DPTDateTime(DPTComplex[KNXDateTime]):
         try:
             cls._test_range(knx_date_time)
         except ValueError as err:
-            raise ConversionError(f"Could not parse {cls.__name__}: {err}") from err
+            raise ConversionError(f"Could not parse {cls.dpt_name()}: {err}") from err
         return knx_date_time
 
     @classmethod
@@ -159,7 +159,9 @@ class DPTDateTime(DPTComplex[KNXDateTime]):
         try:
             cls._test_range(value)
         except ValueError as err:
-            raise ConversionError(f"Could not serialize {cls.__name__}: {err}") from err
+            raise ConversionError(
+                f"Could not serialize {cls.dpt_name()}: {err}"
+            ) from err
 
         knx_year = (value.year - 1900) & 0xFF if value.year is not None else 0
 
diff --git a/xknx/dpt/dpt_5.py b/xknx/dpt/dpt_5.py
index d5b2a1f2..27b99566 100644
--- a/xknx/dpt/dpt_5.py
+++ b/xknx/dpt/dpt_5.py
@@ -31,7 +31,7 @@ class DPTValue1ByteUnsigned(DPTNumeric):
 
         if not cls._test_boundaries(value):
             raise ConversionError(
-                f"Could not parse {cls.__name__}", value=value, payload=payload
+                f"Could not parse {cls.dpt_name()}", value=value, payload=payload
             )
 
         return value
@@ -46,7 +46,7 @@ class DPTValue1ByteUnsigned(DPTNumeric):
             return DPTArray(knx_value)
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
     @classmethod
@@ -81,7 +81,7 @@ class DPTScaling(DPTNumeric):
 
         if not cls._test_boundaries(value):
             raise ConversionError(
-                f"Could not parse {cls.__name__}", value=value, payload=payload
+                f"Could not parse {cls.dpt_name()}", value=value, payload=payload
             )
         return value
 
@@ -98,7 +98,7 @@ class DPTScaling(DPTNumeric):
             return DPTArray(knx_value)
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
     @classmethod
diff --git a/xknx/dpt/dpt_6.py b/xknx/dpt/dpt_6.py
index ee52ccb0..3bf176c9 100644
--- a/xknx/dpt/dpt_6.py
+++ b/xknx/dpt/dpt_6.py
@@ -44,7 +44,7 @@ class DPTSignedRelativeValue(DPTNumeric):
             return DPTArray(knx_value & 0xFF)
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
     @classmethod
diff --git a/xknx/dpt/dpt_7.py b/xknx/dpt/dpt_7.py
index fd0f85e1..20f984cf 100644
--- a/xknx/dpt/dpt_7.py
+++ b/xknx/dpt/dpt_7.py
@@ -42,7 +42,7 @@ class DPT2ByteUnsigned(DPTNumeric):
             return DPTArray((knx_value >> 8, knx_value & 0xFF))
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
     @classmethod
diff --git a/xknx/dpt/dpt_8.py b/xknx/dpt/dpt_8.py
index 2102ea40..ea669da4 100644
--- a/xknx/dpt/dpt_8.py
+++ b/xknx/dpt/dpt_8.py
@@ -42,7 +42,7 @@ class DPT2ByteSigned(DPTNumeric):
         try:
             return struct.unpack(cls._struct_format, bytes(raw))[0] * cls.resolution  # type: ignore[no-any-return]
         except struct.error as err:
-            raise ConversionError(f"Could not parse {cls.__name__}", raw=raw) from err
+            raise ConversionError(f"Could not parse {cls.dpt_name()}", raw=raw) from err
 
     @classmethod
     def to_knx(cls, value: int | float) -> DPTArray:
@@ -54,7 +54,7 @@ class DPT2ByteSigned(DPTNumeric):
             return DPTArray(struct.pack(cls._struct_format, knx_value))
         except (ValueError, struct.error) as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
 
diff --git a/xknx/dpt/dpt_9.py b/xknx/dpt/dpt_9.py
index b006280d..0be1825a 100644
--- a/xknx/dpt/dpt_9.py
+++ b/xknx/dpt/dpt_9.py
@@ -43,7 +43,7 @@ class DPT2ByteFloat(DPTNumeric):
         value = float(significand << exponent) / 100
 
         if not cls._test_boundaries(value):
-            raise ConversionError(f"Could not parse {cls.__name__}", value=value)
+            raise ConversionError(f"Could not parse {cls.dpt_name()}", value=value)
 
         return value
 
@@ -77,7 +77,7 @@ class DPT2ByteFloat(DPTNumeric):
             return DPTArray((msb, mantisse & 0xFF))
         except ValueError as err:
             raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
+                f"Could not serialize {cls.dpt_name()}", value=value
             ) from err
 
     @classmethod
diff --git a/xknx/remote_value/remote_value_sensor.py b/xknx/remote_value/remote_value_sensor.py
index d9f168b4..3cb75d21 100644
--- a/xknx/remote_value/remote_value_sensor.py
+++ b/xknx/remote_value/remote_value_sensor.py
@@ -46,7 +46,7 @@ class _RemoteValueGeneric(RemoteValue[ValueT]):
         )
         if _dpt_class is None:
             raise ConversionError(
-                f"invalid value type for base class {self.dpt_base_class.__name__}",
+                f"invalid value type for base class {self.dpt_base_class.dpt_name()}",
                 value_type=value_type,
                 device_name=device_name,
             )
diff --git a/xknx/telegram/telegram.py b/xknx/telegram/telegram.py
index 305b589a..13de0674 100644
--- a/xknx/telegram/telegram.py
+++ b/xknx/telegram/telegram.py
@@ -45,7 +45,7 @@ class TelegramDecodedData:
         """Return object as readable string."""
         return (
             f"{self.value}{' ' + self.transcoder.unit if self.transcoder.unit is not None else ''}"
-            f" ({self.transcoder.__name__})"
+            f" ({self.transcoder.dpt_name()})"
         )
 
 


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index e31323a8..56fbbd35 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -1,13 +1,13 @@
 -r production.txt
 freezegun==1.5.1
-mypy==1.14.1
+mypy==1.15.0
 pre-commit==4.1.0
 pylint==3.3.4
 pytest==8.3.4
 pytest-asyncio==0.25.3
 pytest-cov==6.0.0
 pytest-icdiff==0.9
-ruff==0.9.4
+ruff==0.9.5
 setuptools==75.8.0
 tox==4.24.1
 tox-gh-actions==3.2.0
diff --git a/test/core_tests/group_address_dpt_test.py b/test/core_tests/group_address_dpt_test.py
index 8b4afce0..49008828 100644
--- a/test/core_tests/group_address_dpt_test.py
+++ b/test/core_tests/group_address_dpt_test.py
@@ -45,9 +45,9 @@ async def test_group_address_dpt_in_telegram_queue(xknx_no_interface: XKNX) -> N
         assert test_telegram.decoded_data is None
 
 
-@patch("logging.Logger.warning")
+@patch("logging.Logger.debug")
 async def test_get_invalid_payload(
-    logger_warning_mock: Mock, xknx_no_interface: XKNX
+    logger_debug_mock: Mock, xknx_no_interface: XKNX
 ) -> None:
     """Test processing when DPT doesn't fit payload."""
     xknx = xknx_no_interface
@@ -62,12 +62,13 @@ async def test_get_invalid_payload(
     )
     async with xknx:
         xknx.telegrams.put_nowait(telegram)
+        logger_debug_mock.reset_mock()
         await xknx.telegrams.join()
         assert telegram_callback.call_count == 1
         test_telegram = telegram_callback.call_args[0][0]
         assert test_telegram.decoded_data is None
-        assert logger_warning_mock.call_count == 1
-        assert "DPT decoding error" in logger_warning_mock.call_args[0][0]
+        assert logger_debug_mock.call_count == 2
+        assert "DPT decoding error" in logger_debug_mock.call_args_list[0][0][0]
 
 
 def test_set(xknx_no_interface: XKNX) -> None:
diff --git a/test/dpt_tests/dpt_test.py b/test/dpt_tests/dpt_test.py
index 148c4f06..8d7a343e 100644
--- a/test/dpt_tests/dpt_test.py
+++ b/test/dpt_tests/dpt_test.py
@@ -1,5 +1,6 @@
 """Unit test for KNX binary/integer objects."""
 
+from inspect import isabstract
 from typing import Any
 
 import pytest
@@ -11,6 +12,7 @@ from xknx.dpt import (
     DPTBinary,
     DPTColorRGBW,
     DPTComplex,
+    DPTConsumerProducer,
     DPTEnum,
     DPTHVACContrMode,
     DPTNumeric,
@@ -28,6 +30,8 @@ class TestDPTBase:
         """Test if abstract base classes are ignored by dpt_class_tree and __recursive_subclasses__."""
         for dpt in DPTBase.dpt_class_tree():
             assert dpt not in (DPTBase, DPTNumeric, DPTEnum, DPTComplex)
+            assert not isabstract(dpt)
+            assert dpt()  # test for abstract class - to be removed if instantiation is not allowed anymore
 
     def test_dpt_concrete_subclasses_included(self) -> None:
         """Test if concrete subclasses are included by dpt_class_tree."""
@@ -38,6 +42,7 @@ class TestDPTBase:
             DPTScaling,
             DPTHVACContrMode,
             DPTColorRGBW,
+            DPTConsumerProducer,
         ):
             assert dpt in DPTBase.dpt_class_tree()
 
@@ -51,18 +56,21 @@ class TestDPTBase:
     def test_dpt_subclasses_definition_types(self) -> None:
         """Test value_type and dpt_*_number values for correct type in subclasses of DPTBase."""
         for dpt in DPTBase.dpt_class_tree():
-            if dpt.value_type is not None:
-                assert isinstance(dpt.value_type, str), (
-                    f"Wrong type for value_type in {dpt} : {type(dpt.value_type)} - str `None` expected"
-                )
-            if dpt.dpt_main_number is not None:
-                assert isinstance(dpt.dpt_main_number, int), (
-                    f"Wrong type for dpt_main_number in {dpt} : {type(dpt.dpt_main_number)} - int or `None` expected"
-                )
-            if dpt.dpt_sub_number is not None:
-                assert isinstance(dpt.dpt_sub_number, int), (
-                    f"Wrong type for dpt_sub_number in {dpt} : {type(dpt.dpt_sub_number)} - int or `None` expected"
-                )
+            assert isinstance(dpt.value_type, str), (
+                f"Wrong type for value_type in {dpt} : {type(dpt.value_type)} - str expected"
+            )
+            assert dpt.value_type, f"Empty string for value_type in {dpt} not allowed"
+
+            assert isinstance(dpt.dpt_main_number, int), (
+                f"Wrong type for dpt_main_number in {dpt} : {type(dpt.dpt_main_number)} - int expected"
+            )
+            assert dpt.dpt_main_number, (
+                f"Zero value for dpt_main_number in {dpt} not allowed"
+            )
+
+            assert isinstance(dpt.dpt_sub_number, int | type(None)), (
+                f"Wrong type for dpt_sub_number in {dpt} : {type(dpt.dpt_sub_number)} - int or `None` expected"
+            )
 
     def test_dpt_subclasses_no_duplicate_value_types(self) -> None:
         """Test for duplicate value_type values in subclasses of DPTBase."""
@@ -71,16 +79,19 @@ class TestDPTBase:
             for dpt in DPTBase.dpt_class_tree()
             if dpt.value_type is not None
         ]
-        assert len(value_types) == len(set(value_types))
+        assert len(value_types) == len(set(value_types)), (
+            f"Duplicate DPT value_types found: { {item for item in value_types if value_types.count(item) > 1} }"
+        )
 
     def test_dpt_subclasses_no_duplicate_dpt_number(self) -> None:
         """Test for duplicate value_type values in subclasses of DPTBase."""
         dpt_tuples = [
             (dpt.dpt_main_number, dpt.dpt_sub_number)
             for dpt in DPTBase.dpt_class_tree()
-            if dpt.dpt_main_number is not None and dpt.dpt_sub_number is not None
         ]
-        assert len(dpt_tuples) == len(set(dpt_tuples))
+        assert len(dpt_tuples) == len(set(dpt_tuples)), (
+            f"Duplicate DPT numbers found: { {item for item in dpt_tuples if dpt_tuples.count(item) > 1} }"
+        )
 
     @pytest.mark.parametrize(
         "equal_dpts",
@@ -89,6 +100,7 @@ class TestDPTBase:
             ["2byte_unsigned", 7, "DPT-7", {"main": 7}, {"main": "7", "sub": None}],
             ["temperature", "9.001", {"main": 9, "sub": 1}, {"main": "9", "sub": "1"}],
             ["active_energy", "13.010", {"main": 13, "sub": 10}],
+            ["consumer_producer", "1.1200", {"main": 1, "sub": 1200}],
         ],
     )
     def test_dpt_alternative_notations(self, equal_dpts: list[Any]) -> None:
@@ -131,6 +143,15 @@ class TestDPTBase:
         """Test parsing invalid data."""
         assert DPTBase.parse_transcoder(value) is None
 
+    def test_dpt_name(self) -> None:
+        """Test DPT name."""
+        assert DPTBase.dpt_name() == "DPTBase (abstract)"
+        assert DPTNumeric.dpt_name() == "DPTNumeric (abstract)"
+        assert DPT2ByteFloat.dpt_name() == "DPT2ByteFloat (9)"
+        assert DPTString.dpt_name() == "DPTString (16.000)"
+        assert DPTColorRGBW.dpt_name() == "DPTColorRGBW (251.600)"
+        assert DPTConsumerProducer.dpt_name() == "DPTConsumerProducer (1.1200)"
+
 
 class TestDPTBaseSubclass:
     """Test subclass of transcoder base object."""
@@ -139,7 +160,13 @@ class TestDPTBaseSubclass:
     def test_required_values(self, dpt_class: type[DPTBase]) -> None:
         """Test required class variables are set for definitions."""
         assert dpt_class.payload_type in (DPTArray, DPTBinary)
-        assert dpt_class.payload_length is not None
+        assert isinstance(dpt_class.payload_length, int)
+        assert dpt_class.payload_length, "Payload_length 0 is invalid"
+        assert isinstance(dpt_class.dpt_main_number, int)
+        assert dpt_class.dpt_main_number, "DPT main number 0 is invalid"
+        assert isinstance(dpt_class.dpt_sub_number, int | type(None))
+        assert isinstance(dpt_class.value_type, str)
+        assert dpt_class.value_type, "Empty string for value_type is invalid"
 
     def test_validate_payload_array(self) -> None:
         """Test validate_payload method."""