Repository: XKNX/xknx
Pull request #1492 (2024-06-17 20:21:52)

Rename DPT module names for main-DPT number
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->


Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] 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
- [x] The changes are documented in the changelog (docs/changelog.md)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/XKNX/xknx/pull/1492?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.64%. Comparing base [(`398a333`)](https://app.codecov.io/gh/XKNX/xknx/commit/398a3335a3ebf543bd52fb74bd908fa4930001b8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`e19614a`)](https://app.codecov.io/gh/XKNX/xknx/commit/e19614adf5bfeb8646f9be44b7a32aead9af945e?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/1492/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/1492?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1492   +/-   ##
=======================================
  Coverage   96.63%   96.64%           
=======================================
  Files         154      157    +3     
  Lines       10026    10041   +15     
=======================================
+ Hits         9689     9704   +15     
  Misses        337      337           
```


| [Files](https://app.codecov.io/gh/XKNX/xknx/pull/1492?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/devices/climate\_mode.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?src=pr&el=tree&filepath=xknx%2Fdevices%2Fclimate_mode.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2NsaW1hdGVfbW9kZS5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/devices/light.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?src=pr&el=tree&filepath=xknx%2Fdevices%2Flight.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2xpZ2h0LnB5) | `99.19% <100.00%> (ø)` | |
| [xknx/dpt/\_\_init\_\_.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?src=pr&el=tree&filepath=xknx%2Fdpt%2F__init__.py&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\_10.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?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) | `89.36% <ø> (ø)` | |
| [xknx/dpt/dpt\_11.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?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) | `90.69% <ø> (ø)` | |
| [xknx/dpt/dpt\_12.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_12.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzEyLnB5) | `100.00% <ø> (ø)` | |
| [xknx/dpt/dpt\_13.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_13.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzEzLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_14.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?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% <ø> (ø)` | |
| [xknx/dpt/dpt\_16.py](https://app.codecov.io/gh/XKNX/xknx/pull/1492?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/1492?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% <100.00%> (ø)` | |
| ... and [16 more](https://app.codecov.io/gh/XKNX/xknx/pull/1492?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 423d4a5f..391c14c2 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -19,6 +19,7 @@ nav_order: 2
   - Devices: `.process`
   - RemoteValue: `.set`, `.respond`, `.process` and `.update_value`
   - ValueReader: `.send_group_read`
+- Rename DPT transcoder modules for schema `xknx.dpt.dpt_<main-number>.py`
 
 ### Features
 
diff --git a/examples/example_light_rgbw.py b/examples/example_light_rgbw.py
index c934d3d1..6acc9d27 100644
--- a/examples/example_light_rgbw.py
+++ b/examples/example_light_rgbw.py
@@ -3,7 +3,7 @@
 import asyncio
 
 from xknx import XKNX
-from xknx.dpt.dpt_color import RGBWColor
+from xknx.dpt import RGBWColor
 from xknx.remote_value import RemoteValueColorRGBW
 
 
diff --git a/xknx/devices/climate_mode.py b/xknx/devices/climate_mode.py
index 58672c3d..27687c77 100644
--- a/xknx/devices/climate_mode.py
+++ b/xknx/devices/climate_mode.py
@@ -11,7 +11,7 @@ from collections.abc import Iterator
 from itertools import chain
 from typing import TYPE_CHECKING, Any
 
-from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
+from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import DeviceIllegalValue
 from xknx.remote_value import GroupAddressesType, RemoteValue
 from xknx.remote_value.remote_value_climate_mode import (
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index c540dbe1..2f641d8c 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -20,7 +20,7 @@ from itertools import chain
 import logging
 from typing import TYPE_CHECKING, Any, cast
 
-from xknx.dpt.dpt_color import RGBColor, RGBWColor, XYYColor
+from xknx.dpt import RGBColor, RGBWColor, XYYColor
 from xknx.remote_value import (
     GroupAddressesType,
     RemoteValue,
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index 0543456a..b955f6bc 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -5,19 +5,56 @@ Module for encoding and decoding KNX datatypes.
 * Derived KNX Values like Scaling, Temperature
 """
 
-# flake8: noqa
-from .dpt import DPTBase, DPTNumeric, DPTComplexData, DPTComplex
-from .dpt_1byte_signed import DPTPercentV8, DPTSignedRelativeValue, DPTValue1Count
-from .dpt_1byte_uint import (
+# ruff: noqa: F401
+from .dpt import DPTBase, DPTComplex, DPTComplexData, DPTNumeric
+from .dpt_3 import (
+    DPTControlStartStop,
+    DPTControlStartStopBlinds,
+    DPTControlStartStopDimming,
+    DPTControlStepCode,
+    DPTControlStepwise,
+    DPTControlStepwiseBlinds,
+    DPTControlStepwiseDimming,
+)
+from .dpt_5 import (
+    DPTAngle,
     DPTDecimalFactor,
     DPTPercentU8,
-    DPTSceneNumber,
+    DPTScaling,
     DPTTariff,
     DPTValue1ByteUnsigned,
     DPTValue1Ucount,
 )
-from .dpt_235 import DPTTariffActiveEnergy, TariffActiveEnergy
-from .dpt_2byte_float import (
+from .dpt_6 import DPTPercentV8, DPTSignedRelativeValue, DPTValue1Count
+from .dpt_7 import (
+    DPT2ByteUnsigned,
+    DPT2Ucount,
+    DPTBrightness,
+    DPTColorTemperature,
+    DPTLengthMm,
+    DPTPropDataType,
+    DPTTimePeriod10Msec,
+    DPTTimePeriod100Msec,
+    DPTTimePeriodHrs,
+    DPTTimePeriodMin,
+    DPTTimePeriodMsec,
+    DPTTimePeriodSec,
+    DPTUElCurrentmA,
+)
+from .dpt_8 import (
+    DPT2ByteSigned,
+    DPTDeltaTime10Msec,
+    DPTDeltaTime100Msec,
+    DPTDeltaTimeHrs,
+    DPTDeltaTimeMin,
+    DPTDeltaTimeMsec,
+    DPTDeltaTimeSec,
+    DPTLengthM,
+    DPTPercentV16,
+    DPTRotationAngle,
+    DPTValue2Count,
+)
+from .dpt_9 import (
     DPT2ByteFloat,
     DPTAbsoluteHumidity,
     DPTAirFlow,
@@ -43,44 +80,31 @@ from .dpt_2byte_float import (
     DPTWsp,
     DPTWspKmh,
 )
-from .dpt_2byte_signed import (
-    DPT2ByteSigned,
-    DPTDeltaTime10Msec,
-    DPTDeltaTime100Msec,
-    DPTDeltaTimeHrs,
-    DPTDeltaTimeMin,
-    DPTDeltaTimeMsec,
-    DPTDeltaTimeSec,
-    DPTLengthM,
-    DPTPercentV16,
-    DPTRotationAngle,
-    DPTValue2Count,
-)
-from .dpt_2byte_uint import (
-    DPT2ByteUnsigned,
-    DPT2Ucount,
-    DPTBrightness,
-    DPTColorTemperature,
-    DPTLengthMm,
-    DPTPropDataType,
-    DPTTimePeriod10Msec,
-    DPTTimePeriod100Msec,
-    DPTTimePeriodHrs,
-    DPTTimePeriodMin,
-    DPTTimePeriodMsec,
-    DPTTimePeriodSec,
-    DPTUElCurrentmA,
+from .dpt_10 import DPTTime
+from .dpt_11 import DPTDate
+from .dpt_12 import (
+    DPT4ByteUnsigned,
+    DPTLongTimePeriodHrs,
+    DPTLongTimePeriodMin,
+    DPTLongTimePeriodSec,
+    DPTValue4Ucount,
+    DPTVolumeLiquidLitre,
+    DPTVolumeM3,
 )
-from .dpt_4bit_control import (
-    DPTControlStartStop,
-    DPTControlStartStopBlinds,
-    DPTControlStartStopDimming,
-    DPTControlStepCode,
-    DPTControlStepwise,
-    DPTControlStepwiseBlinds,
-    DPTControlStepwiseDimming,
+from .dpt_13 import (
+    DPT4ByteSigned,
+    DPTActiveEnergy,
+    DPTActiveEnergykWh,
+    DPTActiveEnergyMWh,
+    DPTApparantEnergy,
+    DPTApparantEnergykVAh,
+    DPTFlowRateM3H,
+    DPTLongDeltaTimeSec,
+    DPTReactiveEnergy,
+    DPTReactiveEnergykVARh,
+    DPTValue4Count,
 )
-from .dpt_4byte_float import (
+from .dpt_14 import (
     DPT4ByteFloat,
     DPTAbsoluteTemperature,
     DPTAcceleration,
@@ -164,38 +188,12 @@ from .dpt_4byte_float import (
     DPTWeight,
     DPTWork,
 )
-from .dpt_4byte_int import (
-    DPT4ByteSigned,
-    DPT4ByteUnsigned,
-    DPTActiveEnergy,
-    DPTActiveEnergykWh,
-    DPTActiveEnergyMWh,
-    DPTApparantEnergy,
-    DPTApparantEnergykVAh,
-    DPTFlowRateM3H,
-    DPTLongDeltaTimeSec,
-    DPTLongTimePeriodHrs,
-    DPTLongTimePeriodMin,
-    DPTLongTimePeriodSec,
-    DPTReactiveEnergy,
-    DPTReactiveEnergykVARh,
-    DPTValue4Count,
-    DPTValue4Ucount,
-    DPTVolumeLiquidLitre,
-    DPTVolumeM3,
-)
-from .dpt_color import (
-    DPTColorXYY,
-    DPTColorRGB,
-    RGBColor,
-    XYYColor,
-    RGBWColor,
-    DPTColorRGBW,
-)
-from .dpt_date import DPTDate
-from .dpt_datetime import DPTDateTime
-from .dpt_hvac_mode import DPTControllerStatus, DPTHVACContrMode, DPTHVACMode
-from .dpt_scaling import DPTAngle, DPTScaling
-from .dpt_string import DPTLatin1, DPTString
-from .dpt_time import DPTTime
+from .dpt_16 import DPTLatin1, DPTString
+from .dpt_17 import DPTSceneNumber
+from .dpt_19 import DPTDateTime
+from .dpt_20 import DPTControllerStatus, DPTHVACContrMode, DPTHVACMode
+from .dpt_232 import DPTColorRGB, RGBColor
+from .dpt_235 import DPTTariffActiveEnergy, TariffActiveEnergy
+from .dpt_242 import DPTColorXYY, XYYColor
+from .dpt_251 import DPTColorRGBW, RGBWColor
 from .payload import DPTArray, DPTBinary
diff --git a/xknx/dpt/dpt_time.py b/xknx/dpt/dpt_10.py
similarity index 100%
rename from xknx/dpt/dpt_time.py
rename to xknx/dpt/dpt_10.py
diff --git a/xknx/dpt/dpt_date.py b/xknx/dpt/dpt_11.py
similarity index 100%
rename from xknx/dpt/dpt_date.py
rename to xknx/dpt/dpt_11.py
diff --git a/xknx/dpt/dpt_4byte_int.py b/xknx/dpt/dpt_12.py
similarity index 52%
rename from xknx/dpt/dpt_4byte_int.py
rename to xknx/dpt/dpt_12.py
index 3f68e01d..ce560233 100644
--- a/xknx/dpt/dpt_4byte_int.py
+++ b/xknx/dpt/dpt_12.py
@@ -1,10 +1,4 @@
-"""
-Implementation of Basic KNX 4-Byte Signed and Unsigned Values.
-
-They correspond the following KNX DPTs:
-    12.yyy 4-byte/octet unsigned value, i.e. pulse counter
-    13.yyy 4-byte/octet signed (2's complement), i.e. flow, energy
-"""
+"""Implementation of Basic KNX 4-Byte unsigned integer values."""
 
 from __future__ import annotations
 
@@ -63,24 +57,6 @@ class DPT4ByteUnsigned(DPTNumeric):
         return cls.value_min <= value <= cls.value_max
 
 
-class DPT4ByteSigned(DPT4ByteUnsigned):
-    """
-    Abstraction for KNX 4 Byte "32-bit signed".
-
-    DPT 13.***
-    """
-
-    dpt_main_number = 13
-    dpt_sub_number: int | None = None
-    value_type = "4byte_signed"
-
-    value_min = -2147483648
-    value_max = 2147483647
-    resolution = 1
-
-    _struct_format = ">i"
-
-
 class DPTValue4Ucount(DPT4ByteUnsigned):
     """DPT 12.001 DPT_Value_4_Ucount."""
 
@@ -133,95 +109,3 @@ class DPTVolumeM3(DPT4ByteUnsigned):
     dpt_sub_number = 1201
     value_type = "volume_m3"
     unit = "m³"
-
-
-class DPTValue4Count(DPT4ByteSigned):
-    """DPT 13.001 DPT_Value_4_Count (pulse)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 1
-    value_type = "pulse_4byte"
-    unit = "counter pulses"
-
-
-class DPTFlowRateM3H(DPT4ByteSigned):
-    """DPT 13.002 DPT_FlowRate_m3/h (m³/h)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 2
-    value_type = "flow_rate_m3h"
-    unit = "m³/h"
-
-
-class DPTActiveEnergy(DPT4ByteSigned):
-    """DPT 13.010 DPT_ActiveEnergy (Wh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 10
-    value_type = "active_energy"
-    unit = "Wh"
-    ha_device_class = "energy"
-
-
-class DPTApparantEnergy(DPT4ByteSigned):
-    """DPT 13.011 DPT_ActiveEnergy (VAh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 11
-    value_type = "apparant_energy"
-    unit = "VAh"
-
-
-class DPTReactiveEnergy(DPT4ByteSigned):
-    """DPT 13.012 DPT_ActiveEnergy (VARh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 12
-    value_type = "reactive_energy"
-    unit = "VARh"
-
-
-class DPTActiveEnergykWh(DPT4ByteSigned):
-    """DPT 13.013 DPT_ActiveEnergy_kWh (kWh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 13
-    value_type = "active_energy_kwh"
-    unit = "kWh"
-    ha_device_class = "energy"
-
-
-class DPTApparantEnergykVAh(DPT4ByteSigned):
-    """DPT 13.014 DPT_ActiveEnergy_kVAh (kVAh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 14
-    value_type = "apparant_energy_kvah"
-    unit = "kVAh"
-
-
-class DPTReactiveEnergykVARh(DPT4ByteSigned):
-    """DPT 13.015 DPT_ActiveEnergy (kVARh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 15
-    value_type = "reactive_energy_kvarh"
-    unit = "kVARh"
-
-
-class DPTActiveEnergyMWh(DPT4ByteSigned):
-    """DPT 13.016 DPT_ActiveEnergy_MWh (MWh)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 16
-    value_type = "active_energy_mwh"
-    unit = "MWh"
-
-
-class DPTLongDeltaTimeSec(DPT4ByteSigned):
-    """DPT 13.100 DPT_LongDeltaTimeSec (s)."""
-
-    dpt_main_number = 13
-    dpt_sub_number = 100
-    value_type = "long_delta_timesec"
-    unit = "s"
diff --git a/xknx/dpt/dpt_13.py b/xknx/dpt/dpt_13.py
new file mode 100644
index 00000000..3809f318
--- /dev/null
+++ b/xknx/dpt/dpt_13.py
@@ -0,0 +1,115 @@
+"""Implementation of Basic KNX 4-Byte signed (2's complement) values."""
+
+from __future__ import annotations
+
+from .dpt_12 import DPT4ByteUnsigned
+
+
+class DPT4ByteSigned(DPT4ByteUnsigned):
+    """
+    Abstraction for KNX 4 Byte "32-bit signed".
+
+    DPT 13.***
+    """
+
+    dpt_main_number = 13
+    dpt_sub_number: int | None = None
+    value_type = "4byte_signed"
+
+    value_min = -2147483648
+    value_max = 2147483647
+    resolution = 1
+
+    _struct_format = ">i"
+
+
+class DPTValue4Count(DPT4ByteSigned):
+    """DPT 13.001 DPT_Value_4_Count (pulse)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 1
+    value_type = "pulse_4byte"
+    unit = "counter pulses"
+
+
+class DPTFlowRateM3H(DPT4ByteSigned):
+    """DPT 13.002 DPT_FlowRate_m3/h (m³/h)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 2
+    value_type = "flow_rate_m3h"
+    unit = "m³/h"
+
+
+class DPTActiveEnergy(DPT4ByteSigned):
+    """DPT 13.010 DPT_ActiveEnergy (Wh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 10
+    value_type = "active_energy"
+    unit = "Wh"
+    ha_device_class = "energy"
+
+
+class DPTApparantEnergy(DPT4ByteSigned):
+    """DPT 13.011 DPT_ActiveEnergy (VAh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 11
+    value_type = "apparant_energy"
+    unit = "VAh"
+
+
+class DPTReactiveEnergy(DPT4ByteSigned):
+    """DPT 13.012 DPT_ActiveEnergy (VARh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 12
+    value_type = "reactive_energy"
+    unit = "VARh"
+
+
+class DPTActiveEnergykWh(DPT4ByteSigned):
+    """DPT 13.013 DPT_ActiveEnergy_kWh (kWh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 13
+    value_type = "active_energy_kwh"
+    unit = "kWh"
+    ha_device_class = "energy"
+
+
+class DPTApparantEnergykVAh(DPT4ByteSigned):
+    """DPT 13.014 DPT_ActiveEnergy_kVAh (kVAh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 14
+    value_type = "apparant_energy_kvah"
+    unit = "kVAh"
+
+
+class DPTReactiveEnergykVARh(DPT4ByteSigned):
+    """DPT 13.015 DPT_ActiveEnergy (kVARh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 15
+    value_type = "reactive_energy_kvarh"
+    unit = "kVARh"
+
+
+class DPTActiveEnergyMWh(DPT4ByteSigned):
+    """DPT 13.016 DPT_ActiveEnergy_MWh (MWh)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 16
+    value_type = "active_energy_mwh"
+    unit = "MWh"
+
+
+class DPTLongDeltaTimeSec(DPT4ByteSigned):
+    """DPT 13.100 DPT_LongDeltaTimeSec (s)."""
+
+    dpt_main_number = 13
+    dpt_sub_number = 100
+    value_type = "long_delta_timesec"
+    unit = "s"
diff --git a/xknx/dpt/dpt_4byte_float.py b/xknx/dpt/dpt_14.py
similarity index 100%
rename from xknx/dpt/dpt_4byte_float.py
rename to xknx/dpt/dpt_14.py
diff --git a/xknx/dpt/dpt_string.py b/xknx/dpt/dpt_16.py
similarity index 100%
rename from xknx/dpt/dpt_string.py
rename to xknx/dpt/dpt_16.py
diff --git a/xknx/dpt/dpt_17.py b/xknx/dpt/dpt_17.py
new file mode 100644
index 00000000..62dd87ce
--- /dev/null
+++ b/xknx/dpt/dpt_17.py
@@ -0,0 +1,48 @@
+"""Implementation of KNX DPT 17 Scene."""
+
+from __future__ import annotations
+
+from xknx.exceptions import ConversionError
+
+from .dpt_5 import DPTValue1ByteUnsigned
+from .payload import DPTArray, DPTBinary
+
+
+class DPTSceneNumber(DPTValue1ByteUnsigned):
+    """
+    Abstraction for KNX 1 Octet Scene Number.
+
+    DPT 17.001
+    """
+
+    dpt_main_number = 17
+    dpt_sub_number = 1
+    value_type = "scene_number"
+
+    value_min = 1
+    value_max = 64
+
+    @classmethod
+    def from_knx(cls, payload: DPTArray | DPTBinary) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        value = cls.validate_payload(payload)[0] + 1
+
+        if not cls._test_boundaries(value):
+            raise ConversionError(
+                f"Could not parse {cls.__name__}", value=value, payload=payload
+            )
+
+        return value
+
+    @classmethod
+    def to_knx(cls, value: int | float) -> DPTArray:
+        """Serialize to KNX/IP raw data."""
+        try:
+            knx_value = int(value) - 1
+            if not cls._test_boundaries(knx_value + 1):
+                raise ValueError("Value out of range")
+            return DPTArray(knx_value)
+        except ValueError as err:
+            raise ConversionError(
+                f"Could not serialize {cls.__name__}", value=value
+            ) from err
diff --git a/xknx/dpt/dpt_datetime.py b/xknx/dpt/dpt_19.py
similarity index 100%
rename from xknx/dpt/dpt_datetime.py
rename to xknx/dpt/dpt_19.py
diff --git a/xknx/dpt/dpt_hvac_mode.py b/xknx/dpt/dpt_20.py
similarity index 100%
rename from xknx/dpt/dpt_hvac_mode.py
rename to xknx/dpt/dpt_20.py
diff --git a/xknx/dpt/dpt_232.py b/xknx/dpt/dpt_232.py
new file mode 100644
index 00000000..ba5493ac
--- /dev/null
+++ b/xknx/dpt/dpt_232.py
@@ -0,0 +1,77 @@
+"""Implementation of KNX RGB color data point type."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+from typing import Any
+
+from .dpt import DPTComplex, DPTComplexData
+from .payload import DPTArray, DPTBinary
+
+
+@dataclass(slots=True)
+class RGBColor(DPTComplexData):
+    """
+    Representation of RGB color.
+
+    `red`: int 0..255
+    `green`: int 0..255
+    `blue`: int 0..255
+    """
+
+    red: int
+    green: int
+    blue: int
+
+    @classmethod
+    def from_dict(cls, data: Mapping[str, Any]) -> RGBColor:
+        """Init from a dictionary."""
+        try:
+            red = int(data["red"])
+            green = int(data["green"])
+            blue = int(data["blue"])
+        except KeyError as err:
+            raise ValueError(f"Missing required key: {err}") from err
+        except (ValueError, TypeError) as err:
+            raise ValueError(f"Invalid value for RGB color: {err}") from err
+
+        return cls(red=red, green=green, blue=blue)
+
+    def as_dict(self) -> dict[str, int]:
+        """Create a JSON serializable dictionary."""
+        return {
+            "red": self.red,
+            "green": self.green,
+            "blue": self.blue,
+        }
+
+
+class DPTColorRGB(DPTComplex[RGBColor]):
+    """Abstraction for KNX 3 octet color RGB (DPT 232.600)."""
+
+    data_type = RGBColor
+    payload_type = DPTArray
+    payload_length = 3
+    dpt_main_number = 232
+    dpt_sub_number = 600
+    value_type = "color_rgb"
+
+    @classmethod
+    def from_knx(cls, payload: DPTArray | DPTBinary) -> RGBColor:
+        """Parse/deserialize from KNX/IP raw data."""
+        raw = cls.validate_payload(payload)
+        return RGBColor(
+            red=raw[0],
+            green=raw[1],
+            blue=raw[2],
+        )
+
+    @classmethod
+    def _to_knx(cls, value: RGBColor) -> DPTArray:
+        """Serialize to KNX/IP raw data."""
+        colors: tuple[int, int, int] = (value.red, value.green, value.blue)
+        for color in colors:
+            if not 0 <= color <= 255:
+                raise ValueError(f"Color value out of range 0..255: {value}")
+        return DPTArray(colors)
diff --git a/xknx/dpt/dpt_235.py b/xknx/dpt/dpt_235.py
index d1c43cfd..ee51d762 100644
--- a/xknx/dpt/dpt_235.py
+++ b/xknx/dpt/dpt_235.py
@@ -6,8 +6,8 @@ from collections.abc import Mapping
 from dataclasses import dataclass
 
 from .dpt import DPTComplex, DPTComplexData
-from .dpt_1byte_uint import DPTTariff
-from .dpt_4byte_int import DPTActiveEnergy
+from .dpt_5 import DPTTariff
+from .dpt_13 import DPTActiveEnergy
 from .payload import DPTArray, DPTBinary
 
 
diff --git a/xknx/dpt/dpt_242.py b/xknx/dpt/dpt_242.py
new file mode 100644
index 00000000..51832f3c
--- /dev/null
+++ b/xknx/dpt/dpt_242.py
@@ -0,0 +1,131 @@
+"""Implementation of KNX XYY color data point type."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+from typing import Any
+
+from .dpt import DPTComplex, DPTComplexData
+from .payload import DPTArray, DPTBinary
+
+
+@dataclass(slots=True)
+class XYYColor(DPTComplexData):
+    """
+    Representation of XY color with brightness.
+
+    `color`: tuple(x-axis, y-axis) each 0..1; None if invalid.
+    `brightness`: int 0..255; None if invalid.
+    """
+
+    color: tuple[float, float] | None = None
+    brightness: int | None = None
+
+    @classmethod
+    def from_dict(cls, data: Mapping[str, Any]) -> XYYColor:
+        """Init from a dictionary."""
+        color = None
+        brightness = data.get("brightness")
+        x_axis = data.get("x_axis")
+        y_axis = data.get("y_axis")
+
+        if x_axis is not None and y_axis is not None:
+            try:
+                color = (float(x_axis), float(y_axis))
+            except ValueError as err:
+                raise ValueError(f"Invalid value for color axis: {err}") from err
+        elif x_axis is not None or y_axis is not None:
+            raise ValueError("Both x_axis and y_axis must be provided")
+
+        if brightness is not None:
+            try:
+                brightness = int(brightness)
+            except ValueError as err:
+                raise ValueError(f"Invalid value for brightness: {err}") from err
+
+        return cls(color=color, brightness=brightness)
+
+    def as_dict(self) -> dict[str, int | float | None]:
+        """Create a JSON serializable dictionary."""
+        return {
+            "x_axis": self.color[0] if self.color is not None else None,
+            "y_axis": self.color[1] if self.color is not None else None,
+            "brightness": self.brightness,
+        }
+
+    def __or__(self, other: XYYColor) -> XYYColor:
+        """Merge two XYYColor objects using only valid values."""
+        return XYYColor(
+            color=other.color if other.color is not None else self.color,
+            brightness=other.brightness
+            if other.brightness is not None
+            else self.brightness,
+        )
+
+
+class DPTColorXYY(DPTComplex[XYYColor]):
+    """Abstraction for KNX 6 octet color xyY (DPT 242.600)."""
+
+    data_type = XYYColor
+    payload_type = DPTArray
+    payload_length = 6
+    dpt_main_number = 242
+    dpt_sub_number = 600
+    value_type = "color_xyy"
+
+    @classmethod
+    def from_knx(cls, payload: DPTArray | DPTBinary) -> XYYColor:
+        """Parse/deserialize from KNX/IP raw data."""
+        raw = cls.validate_payload(payload)
+
+        x_axis_int = raw[0] << 8 | raw[1]
+        y_axis_int = raw[2] << 8 | raw[3]
+        brightness = raw[4]
+
+        color_valid = raw[5] >> 1 & 0b1
+        brightness_valid = raw[5] & 0b1
+
+        return XYYColor(
+            color=(
+                # round to 5 digits for better readability but still preserving precision
+                round(x_axis_int / 0xFFFF, 5),
+                round(y_axis_int / 0xFFFF, 5),
+            )
+            if color_valid
+            else None,
+            brightness=brightness if brightness_valid else None,
+        )
+
+    @classmethod
+    def _to_knx(cls, value: XYYColor) -> DPTArray:
+        """Serialize to KNX/IP raw data."""
+        x_axis, y_axis, brightness = 0, 0, 0
+        color_valid = False
+        brightness_valid = False
+
+        if value.color is not None:
+            for axis in value.color:
+                if not 0 <= axis <= 1:
+                    raise ValueError(
+                        f"Color axis value out of range 0..1: {value.color}"
+                    )
+            x_axis, y_axis = (round(axis * 0xFFFF) for axis in value.color)
+            color_valid = True
+
+        if value.brightness is not None:
+            brightness = value.brightness
+            if not 0 <= brightness <= 255:
+                raise ValueError(f"Brightness out of range 0..255: {brightness}")
+            brightness_valid = True
+
+        return DPTArray(
+            (
+                x_axis >> 8,
+                x_axis & 0xFF,
+                y_axis >> 8,
+                y_axis & 0xFF,
+                brightness,
+                color_valid << 1 | brightness_valid,
+            )
+        )
diff --git a/xknx/dpt/dpt_251.py b/xknx/dpt/dpt_251.py
new file mode 100644
index 00000000..a1772059
--- /dev/null
+++ b/xknx/dpt/dpt_251.py
@@ -0,0 +1,108 @@
+"""Implementation of KNX RGBW color data point type."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+
+from .dpt import DPTComplex, DPTComplexData
+from .payload import DPTArray, DPTBinary
+
+
+@dataclass(slots=True)
+class RGBWColor(DPTComplexData):
+    """
+    Representation of RGBW color.
+
+    `red`: int 0..255; None if invalid
+    `green`: int 0..255; None if invalid
+    `blue`: int 0..255; None if invalid
+    `white`: int 0..255; None if invalid
+    """
+
+    red: int | None = None
+    green: int | None = None
+    blue: int | None = None
+    white: int | None = None
+
+    @classmethod
+    def from_dict(cls, data: Mapping[str, int]) -> RGBWColor:
+        """Init from a dictionary."""
+        result = {}
+        for color in ("red", "green", "blue", "white"):
+            try:
+                value = data.get(color)
+                result[color] = int(value) if value is not None else None
+            except AttributeError as err:
+                raise ValueError(f"Invalid value for color {color}: {err}") from err
+            except ValueError as err:
+                raise ValueError(f"Invalid value for color {color}: {err}") from err
+        return cls(**result)
+
+    def as_dict(self) -> dict[str, int | None]:
+        """Create a JSON serializable dictionary."""
+        return {
+            "red": self.red,
+            "green": self.green,
+            "blue": self.blue,
+            "white": self.white,
+        }
+
+    def __or__(self, other: RGBWColor) -> RGBWColor:
+        """Merge two RGBWColor objects using only valid values."""
+        return RGBWColor(
+            red=other.red if other.red is not None else self.red,
+            green=other.green if other.green is not None else self.green,
+            blue=other.blue if other.blue is not None else self.blue,
+            white=other.white if other.white is not None else self.white,
+        )
+
+
+class DPTColorRGBW(DPTComplex[RGBWColor]):
+    """Abstraction for KNX 6 octet color RGBW (DPT 251.600)."""
+
+    data_type = RGBWColor
+    payload_type = DPTArray
+    payload_length = 6
+    dpt_main_number = 251
+    dpt_sub_number = 600
+    value_type = "color_rgbw"
+
+    @classmethod
+    def from_knx(cls, payload: DPTArray | DPTBinary) -> RGBWColor:
+        """Parse/deserialize from KNX/IP raw data."""
+        raw = cls.validate_payload(payload)
+        red_valid = raw[5] >> 3 & 0b1
+        green_valid = raw[5] >> 2 & 0b1
+        blue_valid = raw[5] >> 1 & 0b1
+        white_valid = raw[5] & 0b1
+        return RGBWColor(
+            red=raw[0] if red_valid else None,
+            green=raw[1] if green_valid else None,
+            blue=raw[2] if blue_valid else None,
+            white=raw[3] if white_valid else None,
+        )
+
+    @classmethod
+    def _to_knx(cls, value: RGBWColor) -> DPTArray:
+        """Serialize to KNX/IP raw data."""
+        for color in (value.red, value.green, value.blue, value.white):
+            if color is None:
+                continue
+            if not 0 <= color <= 255:
+                raise ValueError(f"Color value out of range 0..255: {value}")
+        return DPTArray(
+            (
+                value.red if value.red is not None else 0x00,
+                value.green if value.green is not None else 0x00,
+                value.blue if value.blue is not None else 0x00,
+                value.white if value.white is not None else 0x00,
+                0x00,  # reserved
+                (
+                    (value.red is not None) << 3
+                    | (value.green is not None) << 2
+                    | (value.blue is not None) << 1
+                    | (value.white is not None)
+                ),
+            )
+        )
diff --git a/xknx/dpt/dpt_4bit_control.py b/xknx/dpt/dpt_3.py
similarity index 100%
rename from xknx/dpt/dpt_4bit_control.py
rename to xknx/dpt/dpt_3.py
diff --git a/xknx/dpt/dpt_1byte_uint.py b/xknx/dpt/dpt_5.py
similarity index 73%
rename from xknx/dpt/dpt_1byte_uint.py
rename to xknx/dpt/dpt_5.py
index 1263e7de..2c15388a 100644
--- a/xknx/dpt/dpt_1byte_uint.py
+++ b/xknx/dpt/dpt_5.py
@@ -55,6 +55,75 @@ class DPTValue1ByteUnsigned(DPTNumeric):
         return cls.value_min <= value <= cls.value_max
 
 
+class DPTScaling(DPTNumeric):
+    """
+    Abstraction for KNX 1 Octet Percent.
+
+    DPT 5.001
+    """
+
+    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, payload: DPTArray | DPTBinary) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        knx_value = cls.validate_payload(payload)[0]
+        delta = cls.value_max - cls.value_min
+        value = round((knx_value / 255) * delta) + cls.value_min
+
+        if not cls._test_boundaries(value):
+            raise ConversionError(
+                f"Could not parse {cls.__name__}", value=value, payload=payload
+            )
+        return value
+
+    @classmethod
+    def to_knx(cls, value: float) -> DPTArray:
+        """Serialize to KNX/IP raw data."""
+        try:
+            percent_value = float(value)
+            if not cls._test_boundaries(percent_value):
+                raise ValueError("Value out of range")
+            delta = cls.value_max - cls.value_min
+            knx_value = round((percent_value - cls.value_min) / delta * 255)
+
+            return DPTArray(knx_value)
+        except ValueError as err:
+            raise ConversionError(
+                f"Could not serialize {cls.__name__}", value=value
+            ) from err
+
+    @classmethod
+    def _test_boundaries(cls, value: float) -> bool:
+        """Test if value is within defined range for this object."""
+        return cls.value_min <= value <= cls.value_max
+
+
+class DPTAngle(DPTScaling):
+    """
+    Abstraction for KNX 1 Octet Angle.
+
+    DPT 5.003
+    """
+
+    dpt_main_number = 5
+    dpt_sub_number = 3
+    value_type = "angle"
+    unit = "°"
+
+    value_min = 0
+    value_max = 360
+    resolution = 1
+
+
 class DPTPercentU8(DPTValue1ByteUnsigned):
     """
     Abstraction for KNX 1 Octet Percent.
@@ -104,43 +173,3 @@ class DPTValue1Ucount(DPTValue1ByteUnsigned):
     dpt_sub_number = 10
     value_type = "pulse"
     unit = "counter pulses"
-
-
-class DPTSceneNumber(DPTValue1ByteUnsigned):
-    """
-    Abstraction for KNX 1 Octet Scene Number.
-
-    DPT 17.001
-    """
-
-    dpt_main_number = 17
-    dpt_sub_number = 1
-    value_type = "scene_number"
-
-    value_min = 1
-    value_max = 64
-
-    @classmethod
-    def from_knx(cls, payload: DPTArray | DPTBinary) -> int:
-        """Parse/deserialize from KNX/IP raw data."""
-        value = cls.validate_payload(payload)[0] + 1
-
-        if not cls._test_boundaries(value):
-            raise ConversionError(
-                f"Could not parse {cls.__name__}", value=value, payload=payload
-            )
-
-        return value
-
-    @classmethod
-    def to_knx(cls, value: int | float) -> DPTArray:
-        """Serialize to KNX/IP raw data."""
-        try:
-            knx_value = int(value) - 1
-            if not cls._test_boundaries(knx_value + 1):
-                raise ValueError("Value out of range")
-            return DPTArray(knx_value)
-        except ValueError as err:
-            raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
-            ) from err
diff --git a/xknx/dpt/dpt_1byte_signed.py b/xknx/dpt/dpt_6.py
similarity index 100%
rename from xknx/dpt/dpt_1byte_signed.py
rename to xknx/dpt/dpt_6.py
diff --git a/xknx/dpt/dpt_2byte_uint.py b/xknx/dpt/dpt_7.py
similarity index 100%
rename from xknx/dpt/dpt_2byte_uint.py
rename to xknx/dpt/dpt_7.py
diff --git a/xknx/dpt/dpt_2byte_signed.py b/xknx/dpt/dpt_8.py
similarity index 100%
rename from xknx/dpt/dpt_2byte_signed.py
rename to xknx/dpt/dpt_8.py
diff --git a/xknx/dpt/dpt_2byte_float.py b/xknx/dpt/dpt_9.py
similarity index 100%
rename from xknx/dpt/dpt_2byte_float.py
rename to xknx/dpt/dpt_9.py
diff --git a/xknx/dpt/dpt_color.py b/xknx/dpt/dpt_color.py
deleted file mode 100644
index b47f4c4d..00000000
--- a/xknx/dpt/dpt_color.py
+++ /dev/null
@@ -1,297 +0,0 @@
-"""Implementation of KNX color data point types."""
-
-from __future__ import annotations
-
-from collections.abc import Mapping
-from dataclasses import dataclass
-from typing import Any
-
-from .dpt import DPTComplex, DPTComplexData
-from .payload import DPTArray, DPTBinary
-
-
-@dataclass(slots=True)
-class RGBColor(DPTComplexData):
-    """
-    Representation of RGB color.
-
-    `red`: int 0..255
-    `green`: int 0..255
-    `blue`: int 0..255
-    """
-
-    red: int
-    green: int
-    blue: int
-
-    @classmethod
-    def from_dict(cls, data: Mapping[str, Any]) -> RGBColor:
-        """Init from a dictionary."""
-        try:
-            red = int(data["red"])
-            green = int(data["green"])
-            blue = int(data["blue"])
-        except KeyError as err:
-            raise ValueError(f"Missing required key: {err}") from err
-        except (ValueError, TypeError) as err:
-            raise ValueError(f"Invalid value for RGB color: {err}") from err
-
-        return cls(red=red, green=green, blue=blue)
-
-    def as_dict(self) -> dict[str, int]:
-        """Create a JSON serializable dictionary."""
-        return {
-            "red": self.red,
-            "green": self.green,
-            "blue": self.blue,
-        }
-
-
-class DPTColorRGB(DPTComplex[RGBColor]):
-    """Abstraction for KNX 3 octet color RGB (DPT 232.600)."""
-
-    data_type = RGBColor
-    payload_type = DPTArray
-    payload_length = 3
-    dpt_main_number = 232
-    dpt_sub_number = 600
-    value_type = "color_rgb"
-
-    @classmethod
-    def from_knx(cls, payload: DPTArray | DPTBinary) -> RGBColor:
-        """Parse/deserialize from KNX/IP raw data."""
-        raw = cls.validate_payload(payload)
-        return RGBColor(
-            red=raw[0],
-            green=raw[1],
-            blue=raw[2],
-        )
-
-    @classmethod
-    def _to_knx(cls, value: RGBColor) -> DPTArray:
-        """Serialize to KNX/IP raw data."""
-        colors: tuple[int, int, int] = (value.red, value.green, value.blue)
-        for color in colors:
-            if not 0 <= color <= 255:
-                raise ValueError(f"Color value out of range 0..255: {value}")
-        return DPTArray(colors)
-
-
-@dataclass(slots=True)
-class RGBWColor(DPTComplexData):
-    """
-    Representation of RGBW color.
-
-    `red`: int 0..255; None if invalid
-    `green`: int 0..255; None if invalid
-    `blue`: int 0..255; None if invalid
-    `white`: int 0..255; None if invalid
-    """
-
-    red: int | None = None
-    green: int | None = None
-    blue: int | None = None
-    white: int | None = None
-
-    @classmethod
-    def from_dict(cls, data: Mapping[str, int]) -> RGBWColor:
-        """Init from a dictionary."""
-        result = {}
-        for color in ("red", "green", "blue", "white"):
-            try:
-                value = data.get(color)
-                result[color] = int(value) if value is not None else None
-            except AttributeError as err:
-                raise ValueError(f"Invalid value for color {color}: {err}") from err
-            except ValueError as err:
-                raise ValueError(f"Invalid value for color {color}: {err}") from err
-        return cls(**result)
-
-    def as_dict(self) -> dict[str, int | None]:
-        """Create a JSON serializable dictionary."""
-        return {
-            "red": self.red,
-            "green": self.green,
-            "blue": self.blue,
-            "white": self.white,
-        }
-
-    def __or__(self, other: RGBWColor) -> RGBWColor:
-        """Merge two RGBWColor objects using only valid values."""
-        return RGBWColor(
-            red=other.red if other.red is not None else self.red,
-            green=other.green if other.green is not None else self.green,
-            blue=other.blue if other.blue is not None else self.blue,
-            white=other.white if other.white is not None else self.white,
-        )
-
-
-class DPTColorRGBW(DPTComplex[RGBWColor]):
-    """Abstraction for KNX 6 octet color RGBW (DPT 251.600)."""
-
-    data_type = RGBWColor
-    payload_type = DPTArray
-    payload_length = 6
-    dpt_main_number = 251
-    dpt_sub_number = 600
-    value_type = "color_rgbw"
-
-    @classmethod
-    def from_knx(cls, payload: DPTArray | DPTBinary) -> RGBWColor:
-        """Parse/deserialize from KNX/IP raw data."""
-        raw = cls.validate_payload(payload)
-        red_valid = raw[5] >> 3 & 0b1
-        green_valid = raw[5] >> 2 & 0b1
-        blue_valid = raw[5] >> 1 & 0b1
-        white_valid = raw[5] & 0b1
-        return RGBWColor(
-            red=raw[0] if red_valid else None,
-            green=raw[1] if green_valid else None,
-            blue=raw[2] if blue_valid else None,
-            white=raw[3] if white_valid else None,
-        )
-
-    @classmethod
-    def _to_knx(cls, value: RGBWColor) -> DPTArray:
-        """Serialize to KNX/IP raw data."""
-        for color in (value.red, value.green, value.blue, value.white):
-            if color is None:
-                continue
-            if not 0 <= color <= 255:
-                raise ValueError(f"Color value out of range 0..255: {value}")
-        return DPTArray(
-            (
-                value.red if value.red is not None else 0x00,
-                value.green if value.green is not None else 0x00,
-                value.blue if value.blue is not None else 0x00,
-                value.white if value.white is not None else 0x00,
-                0x00,  # reserved
-                (
-                    (value.red is not None) << 3
-                    | (value.green is not None) << 2
-                    | (value.blue is not None) << 1
-                    | (value.white is not None)
-                ),
-            )
-        )
-
-
-@dataclass(slots=True)
-class XYYColor(DPTComplexData):
-    """
-    Representation of XY color with brightness.
-
-    `color`: tuple(x-axis, y-axis) each 0..1; None if invalid.
-    `brightness`: int 0..255; None if invalid.
-    """
-
-    color: tuple[float, float] | None = None
-    brightness: int | None = None
-
-    @classmethod
-    def from_dict(cls, data: Mapping[str, Any]) -> XYYColor:
-        """Init from a dictionary."""
-        color = None
-        brightness = data.get("brightness")
-        x_axis = data.get("x_axis")
-        y_axis = data.get("y_axis")
-
-        if x_axis is not None and y_axis is not None:
-            try:
-                color = (float(x_axis), float(y_axis))
-            except ValueError as err:
-                raise ValueError(f"Invalid value for color axis: {err}") from err
-        elif x_axis is not None or y_axis is not None:
-            raise ValueError("Both x_axis and y_axis must be provided")
-
-        if brightness is not None:
-            try:
-                brightness = int(brightness)
-            except ValueError as err:
-                raise ValueError(f"Invalid value for brightness: {err}") from err
-
-        return cls(color=color, brightness=brightness)
-
-    def as_dict(self) -> dict[str, int | float | None]:
-        """Create a JSON serializable dictionary."""
-        return {
-            "x_axis": self.color[0] if self.color is not None else None,
-            "y_axis": self.color[1] if self.color is not None else None,
-            "brightness": self.brightness,
-        }
-
-    def __or__(self, other: XYYColor) -> XYYColor:
-        """Merge two XYYColor objects using only valid values."""
-        return XYYColor(
-            color=other.color if other.color is not None else self.color,
-            brightness=other.brightness
-            if other.brightness is not None
-            else self.brightness,
-        )
-
-
-class DPTColorXYY(DPTComplex[XYYColor]):
-    """Abstraction for KNX 6 octet color xyY (DPT 242.600)."""
-
-    data_type = XYYColor
-    payload_type = DPTArray
-    payload_length = 6
-    dpt_main_number = 242
-    dpt_sub_number = 600
-    value_type = "color_xyy"
-
-    @classmethod
-    def from_knx(cls, payload: DPTArray | DPTBinary) -> XYYColor:
-        """Parse/deserialize from KNX/IP raw data."""
-        raw = cls.validate_payload(payload)
-
-        x_axis_int = raw[0] << 8 | raw[1]
-        y_axis_int = raw[2] << 8 | raw[3]
-        brightness = raw[4]
-
-        color_valid = raw[5] >> 1 & 0b1
-        brightness_valid = raw[5] & 0b1
-
-        return XYYColor(
-            color=(
-                # round to 5 digits for better readability but still preserving precision
-                round(x_axis_int / 0xFFFF, 5),
-                round(y_axis_int / 0xFFFF, 5),
-            )
-            if color_valid
-            else None,
-            brightness=brightness if brightness_valid else None,
-        )
-
-    @classmethod
-    def _to_knx(cls, value: XYYColor) -> DPTArray:
-        """Serialize to KNX/IP raw data."""
-        x_axis, y_axis, brightness = 0, 0, 0
-        color_valid = False
-        brightness_valid = False
-
-        if value.color is not None:
-            for axis in value.color:
-                if not 0 <= axis <= 1:
-                    raise ValueError(
-                        f"Color axis value out of range 0..1: {value.color}"
-                    )
-            x_axis, y_axis = (round(axis * 0xFFFF) for axis in value.color)
-            color_valid = True
-
-        if value.brightness is not None:
-            brightness = value.brightness
-            if not 0 <= brightness <= 255:
-                raise ValueError(f"Brightness out of range 0..255: {brightness}")
-            brightness_valid = True
-
-        return DPTArray(
-            (
-                x_axis >> 8,
-                x_axis & 0xFF,
-                y_axis >> 8,
-                y_axis & 0xFF,
-                brightness,
-                color_valid << 1 | brightness_valid,
-            )
-        )
diff --git a/xknx/dpt/dpt_scaling.py b/xknx/dpt/dpt_scaling.py
deleted file mode 100644
index 25641493..00000000
--- a/xknx/dpt/dpt_scaling.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""Implementation of scaled KNX DPT_1_Ucount Values."""
-
-from __future__ import annotations
-
-from xknx.exceptions import ConversionError
-
-from .dpt import DPTNumeric
-from .payload import DPTArray, DPTBinary
-
-
-class DPTScaling(DPTNumeric):
-    """
-    Abstraction for KNX 1 Octet Percent.
-
-    DPT 5.001
-    """
-
-    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, payload: DPTArray | DPTBinary) -> int:
-        """Parse/deserialize from KNX/IP raw data."""
-        knx_value = cls.validate_payload(payload)[0]
-        delta = cls.value_max - cls.value_min
-        value = round((knx_value / 255) * delta) + cls.value_min
-
-        if not cls._test_boundaries(value):
-            raise ConversionError(
-                f"Could not parse {cls.__name__}", value=value, payload=payload
-            )
-        return value
-
-    @classmethod
-    def to_knx(cls, value: float) -> DPTArray:
-        """Serialize to KNX/IP raw data."""
-        try:
-            percent_value = float(value)
-            if not cls._test_boundaries(percent_value):
-                raise ValueError("Value out of range")
-            delta = cls.value_max - cls.value_min
-            knx_value = round((percent_value - cls.value_min) / delta * 255)
-
-            return DPTArray(knx_value)
-        except ValueError as err:
-            raise ConversionError(
-                f"Could not serialize {cls.__name__}", value=value
-            ) from err
-
-    @classmethod
-    def _test_boundaries(cls, value: float) -> bool:
-        """Test if value is within defined range for this object."""
-        return cls.value_min <= value <= cls.value_max
-
-
-class DPTAngle(DPTScaling):
-    """
-    Abstraction for KNX 1 Octet Angle.
-
-    DPT 5.003
-    """
-
-    dpt_main_number = 5
-    dpt_sub_number = 3
-    value_type = "angle"
-    unit = "°"
-
-    value_min = 0
-    value_max = 360
-    resolution = 1
diff --git a/xknx/remote_value/remote_value_climate_mode.py b/xknx/remote_value/remote_value_climate_mode.py
index 665b18fb..6f92162f 100644
--- a/xknx/remote_value/remote_value_climate_mode.py
+++ b/xknx/remote_value/remote_value_climate_mode.py
@@ -17,7 +17,7 @@ from xknx.dpt import (
     DPTHVACContrMode,
     DPTHVACMode,
 )
-from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACModeT, HVACOperationMode
+from xknx.dpt.dpt_20 import HVACControllerMode, HVACModeT, HVACOperationMode
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 from xknx.typing import CallbackType
 
diff --git a/xknx/remote_value/remote_value_color_rgb.py b/xknx/remote_value/remote_value_color_rgb.py
index 4f572cf8..acd7493a 100644
--- a/xknx/remote_value/remote_value_color_rgb.py
+++ b/xknx/remote_value/remote_value_color_rgb.py
@@ -6,7 +6,7 @@ DPT 232.600.
 
 from __future__ import annotations
 
-from xknx.dpt.dpt_color import DPTColorRGB, RGBColor
+from xknx.dpt import DPTColorRGB, RGBColor
 
 from .remote_value import RemoteValue
 
diff --git a/xknx/remote_value/remote_value_color_rgbw.py b/xknx/remote_value/remote_value_color_rgbw.py
index 700dd508..6ba21620 100644
--- a/xknx/remote_value/remote_value_color_rgbw.py
+++ b/xknx/remote_value/remote_value_color_rgbw.py
@@ -8,8 +8,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
-from xknx.dpt import DPTArray, DPTBinary
-from xknx.dpt.dpt_color import DPTColorRGBW, RGBWColor
+from xknx.dpt import DPTArray, DPTBinary, DPTColorRGBW, RGBWColor
 from xknx.typing import CallbackType
 
 from .remote_value import GroupAddressesType, RemoteValue
diff --git a/xknx/remote_value/remote_value_color_xyy.py b/xknx/remote_value/remote_value_color_xyy.py
index 0a92e670..dc628f4c 100644
--- a/xknx/remote_value/remote_value_color_xyy.py
+++ b/xknx/remote_value/remote_value_color_xyy.py
@@ -6,7 +6,7 @@ DPT 242.600.
 
 from __future__ import annotations
 
-from xknx.dpt.dpt_color import DPTColorXYY, XYYColor
+from xknx.dpt import DPTColorXYY, XYYColor
 
 from .remote_value import RemoteValue
 


Test patch:
diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py
index 5a498d91..bd54c131 100644
--- a/test/devices_tests/climate_test.py
+++ b/test/devices_tests/climate_test.py
@@ -17,7 +17,7 @@ from xknx.dpt import (
     DPTTemperature,
     DPTValue1Count,
 )
-from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
+from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import ConversionError, DeviceIllegalValue
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueRead, GroupValueWrite
diff --git a/test/devices_tests/light_test.py b/test/devices_tests/light_test.py
index 1995d081..ff6d90a7 100644
--- a/test/devices_tests/light_test.py
+++ b/test/devices_tests/light_test.py
@@ -6,7 +6,7 @@ from xknx import XKNX
 from xknx.devices import Light
 from xknx.devices.light import ColorTemperatureType
 from xknx.dpt import DPTArray, DPTBinary
-from xknx.dpt.dpt_color import XYYColor
+from xknx.dpt.dpt_242 import XYYColor
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueRead, GroupValueWrite
 
diff --git a/test/dpt_tests/dpt_color_test.py b/test/dpt_tests/dpt_color_test.py
index 76f49205..9d5da01b 100644
--- a/test/dpt_tests/dpt_color_test.py
+++ b/test/dpt_tests/dpt_color_test.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from xknx.dpt.dpt_color import (
+from xknx.dpt import (
     DPTArray,
     DPTColorRGB,
     DPTColorRGBW,
diff --git a/test/dpt_tests/dpt_hvac_mode_test.py b/test/dpt_tests/dpt_hvac_mode_test.py
index 2a51cc69..9087f837 100644
--- a/test/dpt_tests/dpt_hvac_mode_test.py
+++ b/test/dpt_tests/dpt_hvac_mode_test.py
@@ -3,7 +3,7 @@
 import pytest
 
 from xknx.dpt import DPTArray, DPTControllerStatus, DPTHVACMode
-from xknx.dpt.dpt_hvac_mode import HVACOperationMode
+from xknx.dpt.dpt_20 import HVACOperationMode
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 
 
diff --git a/test/remote_value_tests/remote_value_climate_mode_test.py b/test/remote_value_tests/remote_value_climate_mode_test.py
index 1177b13f..841cef7f 100644
--- a/test/remote_value_tests/remote_value_climate_mode_test.py
+++ b/test/remote_value_tests/remote_value_climate_mode_test.py
@@ -4,7 +4,7 @@ import pytest
 
 from xknx import XKNX
 from xknx.dpt import DPTArray, DPTBinary
-from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
+from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 from xknx.remote_value import (
     RemoteValueBinaryHeatCool,
diff --git a/test/remote_value_tests/remote_value_color_rgb_test.py b/test/remote_value_tests/remote_value_color_rgb_test.py
index 580e6e84..7f52da4b 100644
--- a/test/remote_value_tests/remote_value_color_rgb_test.py
+++ b/test/remote_value_tests/remote_value_color_rgb_test.py
@@ -3,8 +3,7 @@
 import pytest
 
 from xknx import XKNX
-from xknx.dpt import DPTArray, DPTBinary
-from xknx.dpt.dpt_color import RGBColor
+from xknx.dpt import DPTArray, DPTBinary, RGBColor
 from xknx.exceptions import ConversionError
 from xknx.remote_value import RemoteValueColorRGB
 from xknx.telegram import GroupAddress, Telegram
diff --git a/test/remote_value_tests/remote_value_color_rgbw_test.py b/test/remote_value_tests/remote_value_color_rgbw_test.py
index a149f43c..cbb7d7c3 100644
--- a/test/remote_value_tests/remote_value_color_rgbw_test.py
+++ b/test/remote_value_tests/remote_value_color_rgbw_test.py
@@ -3,8 +3,7 @@
 import pytest
 
 from xknx import XKNX
-from xknx.dpt import DPTArray, DPTBinary
-from xknx.dpt.dpt_color import RGBWColor
+from xknx.dpt import DPTArray, DPTBinary, RGBWColor
 from xknx.exceptions import ConversionError
 from xknx.remote_value import RemoteValueColorRGBW
 from xknx.telegram import GroupAddress, Telegram
diff --git a/test/remote_value_tests/remote_value_color_xyy_test.py b/test/remote_value_tests/remote_value_color_xyy_test.py
index 3c5a4189..7585e59a 100644
--- a/test/remote_value_tests/remote_value_color_xyy_test.py
+++ b/test/remote_value_tests/remote_value_color_xyy_test.py
@@ -3,8 +3,7 @@
 import pytest
 
 from xknx import XKNX
-from xknx.dpt import DPTArray, DPTBinary
-from xknx.dpt.dpt_color import XYYColor
+from xknx.dpt import DPTArray, DPTBinary, XYYColor
 from xknx.exceptions import ConversionError
 from xknx.remote_value import RemoteValueColorXYY
 from xknx.telegram import GroupAddress, Telegram