Repository: XKNX/xknx
Pull request #1142 (2023-01-09 16:34:35)

Update DPT definitions
<!--
  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 definitions for DPT
- 7.010
- 8.012
- 9.029
- 9.030
- 12.001
- 12.100
- 12.101
- 12.102
- 13.016
- 14.080

Also remove empty-string `unit` assignments in favour of inherited `None`.

Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## 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)


Patch:
diff --git a/changelog.md b/changelog.md
new file mode 120000
index 00000000..1bed66b3
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,1 @@
+docs/changelog.md
\ No newline at end of file
diff --git a/docs/changelog.md b/docs/changelog.md
index bce3274a..271d4931 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -8,6 +8,21 @@ nav_order: 2
 
 # Unreleased changes
 
+### DPTs
+
+- Add definitions for DPTs
+  - 7.010 "prop_data_type"
+  - 8.012 "length_m"
+  - 9.009 "air_flow"
+  - 9.029 "absolute_humidity"
+  - 9.030 "concentration_ugm3"
+  - 12.001 "pulse_4_ucount"
+  - 12.100 "long_time_period_sec"
+  - 12.101 "long_time_period_min"
+  - 12.102 "long_time_period_hrs"
+  - 13.016 "active_energy_mwh"
+  - 14.080 "apparent_power"
+
 ### IP Secure
 
 - SecureRouting: verify MAC of received TimerNotify frames.
@@ -17,6 +32,8 @@ nav_order: 2
 ### Internals
 
 - Move `CEMIFrame`, `CEMIFlags` and `CEMIMessageCode` to xknx.cemi package.
+- Remove `CEMIFrame.telegram` setter in favour of `init_from_telegram()` staticmethod; convert `from_knx()` and `from_knx_data_link_layer()` to staticmethods returning a CEMIFrame.
+- Remove default values for `CEMIFrame` constructor.
 - Parse T_Data_Broadcast TPCI. Forward these telegrams to the Management class.
 - KNXIPHeader total_length is 2 bytes long. There are no reserved bytes.
 - Revert handling L_Data.req frames for incoming device management requests.
diff --git a/xknx/cemi/cemi_frame.py b/xknx/cemi/cemi_frame.py
index cebd9256..03ea95e0 100644
--- a/xknx/cemi/cemi_frame.py
+++ b/xknx/cemi/cemi_frame.py
@@ -16,7 +16,7 @@ from __future__ import annotations
 from xknx.exceptions import ConversionError, UnsupportedCEMIMessage
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import APCI
-from xknx.telegram.tpci import TPCI, TDataBroadcast, TDataGroup
+from xknx.telegram.tpci import TPCI, TDataBroadcast
 
 from .const import CEMIFlags, CEMIMessageCode
 
@@ -26,19 +26,20 @@ class CEMIFrame:
 
     def __init__(
         self,
-        code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
-        flags: int = 0,
-        src_addr: IndividualAddress | None = None,
-        dst_addr: GroupAddress | IndividualAddress | None = None,
-        tpci: TPCI | None = None,
-        payload: APCI | None = None,
+        *,
+        code: CEMIMessageCode,
+        flags: int,
+        src_addr: IndividualAddress,
+        dst_addr: GroupAddress | IndividualAddress,
+        tpci: TPCI,
+        payload: APCI | None,
     ):
         """Initialize CEMIFrame object."""
         self.code = code
         self.flags = flags
-        self.src_addr = src_addr or IndividualAddress(None)
-        self.dst_addr = dst_addr or GroupAddress(None)
-        self.tpci = tpci or TDataGroup()
+        self.src_addr = src_addr
+        self.dst_addr = dst_addr
+        self.tpci = tpci
         self.payload = payload
 
     @staticmethod
@@ -48,31 +49,7 @@ class CEMIFrame:
         src_addr: IndividualAddress | None = None,
     ) -> CEMIFrame:
         """Return CEMIFrame from a Telegram."""
-        cemi = CEMIFrame(
-            code=code,
-            src_addr=src_addr or IndividualAddress(None),
-        )
-        # dst_addr, payload and cmd are set by telegram.setter
-        cemi.telegram = telegram
-        return cemi
-
-    @property
-    def telegram(self) -> Telegram:
-        """Return telegram."""
-
-        return Telegram(
-            destination_address=self.dst_addr,
-            payload=self.payload,
-            source_address=self.src_addr,
-            tpci=self.tpci,
-        )
-
-    @telegram.setter
-    def telegram(self, telegram: Telegram) -> None:
-        """Set telegram."""
-        # TODO: Move to separate function, together with setting of
-        # CEMIMessageCode
-        self.flags = (
+        flags = (
             CEMIFlags.FRAME_TYPE_STANDARD
             | CEMIFlags.DO_NOT_REPEAT
             | CEMIFlags.BROADCAST
@@ -80,23 +57,38 @@ class CEMIFrame:
             | CEMIFlags.CONFIRM_NO_ERROR
             | CEMIFlags.HOP_COUNT_1ST
         )
-
         if isinstance(telegram.destination_address, GroupAddress):
-            self.flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS
+            flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS
             if isinstance(telegram.tpci, TDataBroadcast):
-                self.flags |= CEMIFlags.PRIORITY_SYSTEM
+                flags |= CEMIFlags.PRIORITY_SYSTEM
             else:
-                self.flags |= CEMIFlags.PRIORITY_LOW
+                flags |= CEMIFlags.PRIORITY_LOW
         elif isinstance(telegram.destination_address, IndividualAddress):
-            self.flags |= (
+            flags |= (
                 CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS | CEMIFlags.PRIORITY_SYSTEM
             )
         else:
             raise TypeError()
 
-        self.dst_addr = telegram.destination_address
-        self.tpci = telegram.tpci
-        self.payload = telegram.payload
+        return CEMIFrame(
+            code=code,
+            flags=flags,
+            src_addr=src_addr or telegram.source_address,
+            dst_addr=telegram.destination_address,
+            tpci=telegram.tpci,
+            payload=telegram.payload,
+        )
+
+    @property
+    def telegram(self) -> Telegram:
+        """Return telegram."""
+
+        return Telegram(
+            destination_address=self.dst_addr,
+            payload=self.payload,
+            source_address=self.src_addr,
+            tpci=self.tpci,
+        )
 
     def set_hops(self, hops: int) -> None:
         """Set hops."""
@@ -113,27 +105,29 @@ class CEMIFrame:
             return 10
         raise TypeError("Data TPDU must have a payload; control TPDU must not.")
 
-    def from_knx(self, raw: bytes) -> int:
+    @staticmethod
+    def from_knx(raw: bytes) -> CEMIFrame:
         """Parse/deserialize from KNX/IP raw data."""
         try:
-            self.code = CEMIMessageCode(raw[0])
+            code = CEMIMessageCode(raw[0])
         except ValueError:
             raise UnsupportedCEMIMessage(
                 f"CEMIMessageCode not implemented: {raw[0]} in CEMI: {raw.hex()}"
             )
 
-        if self.code not in (
+        if code in (
             CEMIMessageCode.L_DATA_IND,
             CEMIMessageCode.L_DATA_REQ,
             CEMIMessageCode.L_DATA_CON,
         ):
-            raise UnsupportedCEMIMessage(
-                f"Could not handle CEMIMessageCode: {self.code} / {raw[0]} in CEMI: {raw.hex()}"
-            )
+            return CEMIFrame.from_knx_data_link_layer(raw, code)
 
-        return self.from_knx_data_link_layer(raw)
+        raise UnsupportedCEMIMessage(
+            f"Could not handle CEMIMessageCode: {code} / {raw[0]} in CEMI: {raw.hex()}"
+        )
 
-    def from_knx_data_link_layer(self, cemi: bytes) -> int:
+    @staticmethod
+    def from_knx_data_link_layer(cemi: bytes, code: CEMIMessageCode) -> CEMIFrame:
         """Parse L_DATA_IND, CEMIMessageCode.L_DATA_REQ, CEMIMessageCode.L_DATA_CON."""
         if len(cemi) < 10:
             raise UnsupportedCEMIMessage(
@@ -145,26 +139,24 @@ class CEMIFrame:
         # Additional information is not yet parsed.
         addil = cemi[1]
         # Control field 1 and Control field 2 - first 2 octets after Additional information
-        self.flags = cemi[2 + addil] * 256 + cemi[3 + addil]
+        flags = cemi[2 + addil] * 256 + cemi[3 + addil]
 
-        self.src_addr = IndividualAddress(cemi[4 + addil] * 256 + cemi[5 + addil])
+        src_addr = IndividualAddress(cemi[4 + addil] * 256 + cemi[5 + addil])
 
-        dst_is_group_address = bool(self.flags & CEMIFlags.DESTINATION_GROUP_ADDRESS)
-        dst_raw_address = cemi[6 + addil] * 256 + cemi[7 + addil]
-        self.dst_addr = (
-            GroupAddress(dst_raw_address)
-            if dst_is_group_address
-            else IndividualAddress(dst_raw_address)
+        _dst_is_group_address = bool(flags & CEMIFlags.DESTINATION_GROUP_ADDRESS)
+        _dst_raw_address = cemi[6 + addil] * 256 + cemi[7 + addil]
+        dst_addr: GroupAddress | IndividualAddress = (
+            GroupAddress(_dst_raw_address)
+            if _dst_is_group_address
+            else IndividualAddress(_dst_raw_address)
         )
 
-        npdu_len = cemi[8 + addil]
-
-        tpdu = cemi[9 + addil :]
-        apdu = bytes([tpdu[0] & 0b11]) + tpdu[1:]  # clear TPCI bits
-
-        if len(apdu) != (npdu_len + 1):  # TCPI octet not included in NPDU length
+        _npdu_len = cemi[8 + addil]
+        _tpdu = cemi[9 + addil :]
+        _apdu = bytes([_tpdu[0] & 0b11]) + _tpdu[1:]  # clear TPCI bits
+        if len(_apdu) != (_npdu_len + 1):  # TCPI octet not included in NPDU length
             raise UnsupportedCEMIMessage(
-                f"APDU LEN should be {npdu_len} but is {len(apdu) - 1} in CEMI: {cemi.hex()}"
+                f"APDU LEN should be {_npdu_len} but is {len(_apdu) - 1} in CEMI: {cemi.hex()}"
             )
 
         # TPCI (transport layer control information)
@@ -172,30 +164,45 @@ class CEMIFrame:
         # - no control bit set (data) -> First 6 bit
         # APCI (application layer control information) -> Last  10 bit of TPCI/APCI
         try:
-            self.tpci = TPCI.resolve(
-                raw_tpci=tpdu[0],
-                dst_is_group_address=dst_is_group_address,
-                dst_is_zero=not dst_raw_address,
+            tpci = TPCI.resolve(
+                raw_tpci=_tpdu[0],
+                dst_is_group_address=_dst_is_group_address,
+                dst_is_zero=not _dst_raw_address,
             )
         except ConversionError as err:
-            raise UnsupportedCEMIMessage(f"TPCI not supported: {tpdu[0]:#10b}") from err
+            raise UnsupportedCEMIMessage(
+                f"TPCI not supported: {_tpdu[0]:#10b}"
+            ) from err
 
-        if self.tpci.control:
-            if npdu_len:
+        if tpci.control:
+            if _npdu_len:
                 raise UnsupportedCEMIMessage(
-                    f"Invalid length for control TPDU {self.tpci}: {npdu_len}"
+                    f"Invalid length for control TPDU {tpci}: {_npdu_len}"
                 )
-            return 10 + addil
+            return CEMIFrame(
+                code=code,
+                flags=flags,
+                src_addr=src_addr,
+                dst_addr=dst_addr,
+                tpci=tpci,
+                payload=None,
+            )
 
-        _apci = apdu[0] * 256 + apdu[1]
+        _apci = _apdu[0] * 256 + _apdu[1]
         try:
-            self.payload = APCI.resolve_apci(_apci)
+            payload = APCI.resolve_apci(_apci)
         except ConversionError as err:
             raise UnsupportedCEMIMessage(f"APCI not supported: {_apci:#012b}") from err
+        payload.from_knx(_apdu)
 
-        self.payload.from_knx(apdu)
-
-        return 10 + addil + npdu_len
+        return CEMIFrame(
+            code=code,
+            flags=flags,
+            src_addr=src_addr,
+            dst_addr=dst_addr,
+            tpci=tpci,
+            payload=payload,
+        )
 
     def to_knx(self) -> bytes:
         """Serialize to KNX/IP raw data."""
@@ -205,17 +212,12 @@ class CEMIFrame:
         else:
             if not isinstance(self.payload, APCI):
                 raise ConversionError(
-                    f"Invalid payload set for data TPDU: {self.payload.__class__}"
+                    f"Invalid payload set for data TPDU: {type(self.payload)}"
                 )
             tpdu = self.payload.to_knx()
             tpdu[0] |= self.tpci.to_knx()
             npdu_len = self.payload.calculated_length()
 
-        if not isinstance(self.src_addr, IndividualAddress):
-            raise ConversionError("src_addr invalid")
-        if not isinstance(self.dst_addr, (GroupAddress, IndividualAddress)):
-            raise ConversionError("dst_addr invalid")
-
         return (
             bytes(
                 (
diff --git a/xknx/cemi/cemi_handler.py b/xknx/cemi/cemi_handler.py
index 97205c12..6188eb74 100644
--- a/xknx/cemi/cemi_handler.py
+++ b/xknx/cemi/cemi_handler.py
@@ -39,7 +39,9 @@ class CEMIHandler:
         cemi = CEMIFrame.init_from_telegram(
             telegram=telegram,
             code=CEMIMessageCode.L_DATA_REQ,
-            src_addr=self.xknx.current_address,  # TODO: if Telegram address is 0 ?
+            src_addr=(
+                self.xknx.current_address if telegram.source_address.raw == 0 else None
+            ),
         )
         self._l_data_confirmation_event.clear()
         logger.debug("Outgoing CEMI: %s", cemi)
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index 434f8c29..809fe0c4 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -17,7 +17,9 @@ from .dpt_1byte_uint import (
 )
 from .dpt_2byte_float import (
     DPT2ByteFloat,
+    DPTAbsoluteHumidity,
     DPTAirFlow,
+    DPTConcentrationUGM3,
     DPTCurrent,
     DPTEnthalpy,
     DPTHumidity,
@@ -41,10 +43,13 @@ from .dpt_2byte_float import (
 )
 from .dpt_2byte_signed import (
     DPT2ByteSigned,
+    DPTDeltaTime10Msec,
+    DPTDeltaTime100Msec,
     DPTDeltaTimeHrs,
     DPTDeltaTimeMin,
     DPTDeltaTimeMsec,
     DPTDeltaTimeSec,
+    DPTLengthM,
     DPTPercentV16,
     DPTRotationAngle,
     DPTValue2Count,
@@ -55,6 +60,7 @@ from .dpt_2byte_uint import (
     DPTBrightness,
     DPTColorTemperature,
     DPTLengthMm,
+    DPTPropDataType,
     DPTTimePeriod10Msec,
     DPTTimePeriod100Msec,
     DPTTimePeriodHrs,
@@ -85,6 +91,7 @@ from .dpt_4byte_float import (
     DPTAngularFrequency,
     DPTAngularMomentum,
     DPTAngularVelocity,
+    DPTApparentPower,
     DPTArea,
     DPTCapacitance,
     DPTChargeDensitySurface,
@@ -160,13 +167,20 @@ from .dpt_4byte_int import (
     DPT4ByteUnsigned,
     DPTActiveEnergy,
     DPTActiveEnergykWh,
+    DPTActiveEnergyMWh,
     DPTApparantEnergy,
     DPTApparantEnergykVAh,
     DPTFlowRateM3H,
     DPTLongDeltaTimeSec,
+    DPTLongTimePeriodHrs,
+    DPTLongTimePeriodMin,
+    DPTLongTimePeriodSec,
     DPTReactiveEnergy,
     DPTReactiveEnergykVARh,
     DPTValue4Count,
+    DPTValue4Ucount,
+    DPTVolumeLiquidLitre,
+    DPTVolumeM3,
 )
 from .dpt_color import DPTColorXYY
 from .dpt_date import DPTDate
@@ -175,163 +189,3 @@ 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
-
-__all__ = [
-    "DPT2ByteFloat",
-    "DPT2ByteSigned",
-    "DPT2ByteUnsigned",
-    "DPT2Ucount",
-    "DPT4ByteFloat",
-    "DPT4ByteSigned",
-    "DPT4ByteUnsigned",
-    "DPTAbsoluteTemperature",
-    "DPTAcceleration",
-    "DPTAccelerationAngular",
-    "DPTActivationEnergy",
-    "DPTActiveEnergy",
-    "DPTActiveEnergykWh",
-    "DPTActivity",
-    "DPTAmplitude",
-    "DPTAngle",
-    "DPTAngleDeg",
-    "DPTAngleRad",
-    "DPTAngularFrequency",
-    "DPTAngularMomentum",
-    "DPTAngularVelocity",
-    "DPTApparantEnergy",
-    "DPTApparantEnergykVAh",
-    "DPTArea",
-    "DPTArray",
-    "DPTBase",
-    "DPTBinary",
-    "DPTBrightness",
-    "DPTCapacitance",
-    "DPTChargeDensitySurface",
-    "DPTChargeDensityVolume",
-    "DPTColorTemperature",
-    "DPTColorXYY",
-    "DPTCommonTemperature",
-    "DPTCompressibility",
-    "DPTConductance",
-    "DPTControllerStatus",
-    "DPTCurrent",
-    "DPTDate",
-    "DPTDateTime",
-    "DPTDecimalFactor",
-    "DPTDeltaTimeHrs",
-    "DPTDeltaTimeMin",
-    "DPTDeltaTimeMsec",
-    "DPTDeltaTimeSec",
-    "DPTDensity",
-    "DPTElectricalConductivity",
-    "DPTElectricCharge",
-    "DPTElectricCurrent",
-    "DPTElectricCurrentDensity",
-    "DPTElectricDipoleMoment",
-    "DPTElectricDisplacement",
-    "DPTElectricFieldStrength",
-    "DPTElectricFlux",
-    "DPTElectricFluxDensity",
-    "DPTElectricPolarization",
-    "DPTElectricPotential",
-    "DPTElectricPotentialDifference",
-    "DPTElectromagneticMoment",
-    "DPTElectromotiveForce",
-    "DPTEnergy",
-    "DPTEnthalpy",
-    "DPTFlowRateM3H",
-    "DPTForce",
-    "DPTFrequency",
-    "DPTHeatCapacity",
-    "DPTHeatFlowRate",
-    "DPTHeatQuantity",
-    "DPTHumidity",
-    "DPTHVACContrMode",
-    "DPTHVACMode",
-    "DPTImpedance",
-    "DPTKelvinPerPercent",
-    "DPTLatin1",
-    "DPTLength",
-    "DPTLengthMm",
-    "DPTLightQuantity",
-    "DPTLongDeltaTimeSec",
-    "DPTLuminance",
-    "DPTLuminousFlux",
-    "DPTLuminousIntensity",
-    "DPTLux",
-    "DPTMagneticFieldStrength",
-    "DPTMagneticFlux",
-    "DPTMagneticFluxDensity",
-    "DPTMagneticMoment",
-    "DPTMagneticPolarization",
-    "DPTMagnetization",
-    "DPTMagnetomotiveForce",
-    "DPTMass",
-    "DPTMassFlux",
-    "DPTMol",
-    "DPTMomentum",
-    "DPTNumeric",
-    "DPTPartsPerMillion",
-    "DPTPercentU8",
-    "DPTPercentV16",
-    "DPTPercentV8",
-    "DPTPhaseAngleDeg",
-    "DPTPhaseAngleRad",
-    "DPTPower",
-    "DPTPower2Byte",
-    "DPTPowerDensity",
-    "DPTPowerFactor",
-    "DPTPressure",
-    "DPTPressure2Byte",
-    "DPTRainAmount",
-    "DPTReactance",
-    "DPTReactiveEnergy",
-    "DPTReactiveEnergykVARh",
-    "DPTResistance",
-    "DPTResistivity",
-    "DPTRotationAngle",
-    "DPTScaling",
-    "DPTSceneNumber",
-    "DPTSelfInductance",
-    "DPTSignedRelativeValue",
-    "DPTSolidAngle",
-    "DPTSoundIntensity",
-    "DPTSpeed",
-    "DPTStress",
-    "DPTString",
-    "DPTSurfaceTension",
-    "DPTTariff",
-    "DPTTemperature",
-    "DPTTemperatureA",
-    "DPTTemperatureDifference",
-    "DPTTemperatureDifference2Byte",
-    "DPTTemperatureF",
-    "DPTThermalCapacity",
-    "DPTThermalConductivity",
-    "DPTThermoelectricPower",
-    "DPTTime",
-    "DPTTime1",
-    "DPTTime2",
-    "DPTTimePeriod100Msec",
-    "DPTTimePeriod10Msec",
-    "DPTTimePeriodHrs",
-    "DPTTimePeriodMin",
-    "DPTTimePeriodMsec",
-    "DPTTimePeriodSec",
-    "DPTTimeSeconds",
-    "DPTTorque",
-    "DPTUElCurrentmA",
-    "DPTValue1ByteUnsigned",
-    "DPTValue1Count",
-    "DPTValue1Ucount",
-    "DPTValue2Count",
-    "DPTValue4Count",
-    "DPTVoltage",
-    "DPTVolume",
-    "DPTVolumeFlow",
-    "DPTVolumeFlux",
-    "DPTWeight",
-    "DPTWork",
-    "DPTWsp",
-    "DPTWspKmh",
-]
diff --git a/xknx/dpt/dpt_1byte_signed.py b/xknx/dpt/dpt_1byte_signed.py
index 3b7a7404..aa54c4f8 100644
--- a/xknx/dpt/dpt_1byte_signed.py
+++ b/xknx/dpt/dpt_1byte_signed.py
@@ -16,7 +16,6 @@ class DPTSignedRelativeValue(DPTNumeric):
     dpt_main_number = 6
     dpt_sub_number: int | None = None
     value_type = "1byte_signed"
-    unit = ""
     payload_length = 1
 
     value_min = -128
diff --git a/xknx/dpt/dpt_1byte_uint.py b/xknx/dpt/dpt_1byte_uint.py
index 02e83863..bdba7898 100644
--- a/xknx/dpt/dpt_1byte_uint.py
+++ b/xknx/dpt/dpt_1byte_uint.py
@@ -16,7 +16,6 @@ class DPTValue1ByteUnsigned(DPTNumeric):
     dpt_main_number = 5
     dpt_sub_number: int | None = None
     value_type = "1byte_unsigned"
-    unit = ""
     payload_length = 1
 
     value_min = 0
@@ -115,7 +114,6 @@ class DPTSceneNumber(DPTValue1ByteUnsigned):
     dpt_main_number = 17
     dpt_sub_number = 1
     value_type = "scene_number"
-    unit = ""
 
     value_min = 1
     value_max = 64
diff --git a/xknx/dpt/dpt_2byte_float.py b/xknx/dpt/dpt_2byte_float.py
index b3d46c03..f33c9073 100644
--- a/xknx/dpt/dpt_2byte_float.py
+++ b/xknx/dpt/dpt_2byte_float.py
@@ -20,7 +20,6 @@ class DPT2ByteFloat(DPTNumeric):
     dpt_main_number = 9
     dpt_sub_number: int | None = None
     value_type = "2byte_float"
-    unit = ""
     payload_length = 2
 
     value_min = -671088.64
@@ -298,6 +297,24 @@ class DPTWspKmh(DPT2ByteFloat):
     value_max = 670760
 
 
+class DPTAbsoluteHumidity(DPT2ByteFloat):
+    """DPT 9.029 DPT_Value_Absolute_Humidity."""
+
+    dpt_main_number = 9
+    dpt_sub_number = 29
+    value_type = "absolute_humidity"
+    unit = "g/m³"
+
+
+class DPTConcentrationUGM3(DPT2ByteFloat):
+    """DPT 9.030 DPT_Value_Concentration_μgm3."""
+
+    dpt_main_number = 9
+    dpt_sub_number = 30
+    value_type = "concentration_ugm3"
+    unit = "μg/m³"
+
+
 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 aa92a65f..0e2ef741 100644
--- a/xknx/dpt/dpt_2byte_signed.py
+++ b/xknx/dpt/dpt_2byte_signed.py
@@ -23,7 +23,6 @@ class DPT2ByteSigned(DPTNumeric):
     dpt_main_number = 8
     dpt_sub_number: int | None = None
     value_type = "2byte_signed"
-    unit = ""
     payload_length = 2
 
     value_min = -32768
@@ -138,3 +137,12 @@ class DPTRotationAngle(DPT2ByteSigned):
     dpt_sub_number = 11
     value_type = "rotation_angle"
     unit = "°"
+
+
+class DPTLengthM(DPT2ByteSigned):
+    """DPT 8.012 DPT_Length_m."""
+
+    dpt_main_number = 8
+    dpt_sub_number = 12
+    value_type = "length_m"
+    unit = "m"
diff --git a/xknx/dpt/dpt_2byte_uint.py b/xknx/dpt/dpt_2byte_uint.py
index e244d500..b8a6a2ba 100644
--- a/xknx/dpt/dpt_2byte_uint.py
+++ b/xknx/dpt/dpt_2byte_uint.py
@@ -18,7 +18,6 @@ class DPT2ByteUnsigned(DPTNumeric):
     dpt_main_number = 7
     dpt_sub_number: int | None = None
     value_type = "2byte_unsigned"
-    unit = ""
     payload_length = 2
 
     value_min = 0
@@ -111,6 +110,14 @@ class DPTTimePeriodHrs(DPT2ByteUnsigned):
     unit = "h"
 
 
+class DPTPropDataType(DPT2ByteUnsigned):
+    """DPT 7.010 DPT_PropDataType."""
+
+    dpt_main_number = 7
+    dpt_sub_number = 10
+    value_type = "prop_data_type"
+
+
 class DPTLengthMm(DPT2ByteUnsigned):
     """DPT 7.011 Abstraction for KNX 2 Byte DPT_Length_mm (mm)."""
 
diff --git a/xknx/dpt/dpt_4bit_control.py b/xknx/dpt/dpt_4bit_control.py
index 9cb8e20f..7f4c171f 100644
--- a/xknx/dpt/dpt_4bit_control.py
+++ b/xknx/dpt/dpt_4bit_control.py
@@ -30,7 +30,6 @@ class DPTControlStepCode(DPTBase, ABC):
     APCI_STEPCODEMASK = 0x07
     APCI_MAX_VALUE = APCI_CONTROLMASK | APCI_STEPCODEMASK
 
-    unit = ""
     payload_length = 1
 
     @classmethod
@@ -100,7 +99,6 @@ class DPTControlStepwise(DPTControlStepCode):
     """Abstraction for KNX DPT 3.xxx in stepwise mode with conversion to an incement value."""
 
     dpt_main_number = 3
-    dpt_sub_number: int | None = None
     value_type = "stepwise"
     unit = "%"
 
@@ -184,7 +182,6 @@ class DPTControlStartStop(DPTControlStepCode):
     """Abstraction for KNX DPT 3.xxx in start/stop mode."""
 
     value_type = "startstop"
-    unit = ""
 
     class Direction(TitleEnum):
         """Enum for indicating the direction."""
diff --git a/xknx/dpt/dpt_4byte_float.py b/xknx/dpt/dpt_4byte_float.py
index 3364217c..f1175efc 100644
--- a/xknx/dpt/dpt_4byte_float.py
+++ b/xknx/dpt/dpt_4byte_float.py
@@ -29,7 +29,6 @@ class DPT4ByteFloat(DPTNumeric):
     dpt_main_number = 14
     dpt_sub_number: int | None = None
     value_type = "4byte_float"
-    unit = ""
     payload_length = 4
 
     value_min = float("-inf")
@@ -114,7 +113,6 @@ class DPTAmplitude(DPT4ByteFloat):
     dpt_main_number = 14
     dpt_sub_number = 5
     value_type = "amplitude"
-    unit = ""
 
 
 class DPTAngleRad(DPT4ByteFloat):
@@ -586,7 +584,6 @@ class DPTPowerFactor(DPT4ByteFloat):
     dpt_main_number = 14
     dpt_sub_number = 57
     value_type = "powerfactor"
-    unit = "cosΦ"
     ha_device_class = "power_factor"
 
 
@@ -624,7 +621,7 @@ class DPTResistivity(DPT4ByteFloat):
     dpt_main_number = 14
     dpt_sub_number = 61
     value_type = "resistivity"
-    unit = "Ω m"
+    unit = "Ωm"
 
 
 class DPTSelfInductance(DPT4ByteFloat):
@@ -750,7 +747,7 @@ class DPTTorque(DPT4ByteFloat):
     dpt_main_number = 14
     dpt_sub_number = 75
     value_type = "torque"
-    unit = "N m"
+    unit = "Nm"
 
 
 class DPTVolume(DPT4ByteFloat):
@@ -787,3 +784,12 @@ class DPTWork(DPT4ByteFloat):
     dpt_sub_number = 79
     value_type = "work"
     unit = "J"
+
+
+class DPTApparentPower(DPT4ByteFloat):
+    """DPT 14.080 DPT_Value_Apparent_Power."""
+
+    dpt_main_number = 14
+    dpt_sub_number = 80
+    value_type = "apparent_power"
+    unit = "VA"
diff --git a/xknx/dpt/dpt_4byte_int.py b/xknx/dpt/dpt_4byte_int.py
index 9f23f1d6..9bd052e0 100644
--- a/xknx/dpt/dpt_4byte_int.py
+++ b/xknx/dpt/dpt_4byte_int.py
@@ -24,7 +24,6 @@ class DPT4ByteUnsigned(DPTNumeric):
     dpt_main_number = 12
     dpt_sub_number: int | None = None
     value_type = "4byte_unsigned"
-    unit = ""
     payload_length = 4
 
     value_min = 0
@@ -70,7 +69,6 @@ class DPT4ByteSigned(DPT4ByteUnsigned):
     dpt_main_number = 13
     dpt_sub_number: int | None = None
     value_type = "4byte_signed"
-    unit = ""
 
     value_min = -2147483648
     value_max = 2147483647
@@ -79,6 +77,42 @@ class DPT4ByteSigned(DPT4ByteUnsigned):
     _struct_format = ">i"
 
 
+class DPTValue4Ucount(DPT4ByteUnsigned):
+    """DPT 12.001 DPT_Value_4_Ucount."""
+
+    dpt_main_number = 12
+    dpt_sub_number = 1
+    value_type = "pulse_4_ucount"
+    unit = "counter pulses"
+
+
+class DPTLongTimePeriodSec(DPT4ByteUnsigned):
+    """DPT 12.100 DPT_LongTimePeriod_Sec (seconds)."""
+
+    dpt_main_number = 12
+    dpt_sub_number = 100
+    value_type = "long_time_period_sec"
+    unit = "s"
+
+
+class DPTLongTimePeriodMin(DPT4ByteUnsigned):
+    """DPT 12.101 DPT_LongTimePeriod_Min (minutes)."""
+
+    dpt_main_number = 12
+    dpt_sub_number = 101
+    value_type = "long_time_period_min"
+    unit = "min"
+
+
+class DPTLongTimePeriodHrs(DPT4ByteUnsigned):
+    """DPT 12.102 DPT_LongTimePeriod_Hrs (hours)."""
+
+    dpt_main_number = 12
+    dpt_sub_number = 102
+    value_type = "long_time_period_hrs"
+    unit = "h"
+
+
 class DPTVolumeLiquidLitre(DPT4ByteUnsigned):
     """DPT 12.1200 DPT_VolumeLiquid_Litre (water/gas total consumption)."""
 
@@ -103,7 +137,7 @@ class DPTValue4Count(DPT4ByteSigned):
     dpt_main_number = 13
     dpt_sub_number = 1
     value_type = "pulse_4byte"
-    unit = "pulses"
+    unit = "counter pulses"
 
 
 class DPTFlowRateM3H(DPT4ByteSigned):
@@ -132,7 +166,6 @@ class DPTApparantEnergy(DPT4ByteSigned):
     dpt_sub_number = 11
     value_type = "apparant_energy"
     unit = "VAh"
-    ha_device_class = "energy"
 
 
 class DPTReactiveEnergy(DPT4ByteSigned):
@@ -142,7 +175,6 @@ class DPTReactiveEnergy(DPT4ByteSigned):
     dpt_sub_number = 12
     value_type = "reactive_energy"
     unit = "VARh"
-    ha_device_class = "energy"
 
 
 class DPTActiveEnergykWh(DPT4ByteSigned):
@@ -162,7 +194,6 @@ class DPTApparantEnergykVAh(DPT4ByteSigned):
     dpt_sub_number = 14
     value_type = "apparant_energy_kvah"
     unit = "kVAh"
-    ha_device_class = "energy"
 
 
 class DPTReactiveEnergykVARh(DPT4ByteSigned):
@@ -172,7 +203,15 @@ class DPTReactiveEnergykVARh(DPT4ByteSigned):
     dpt_sub_number = 15
     value_type = "reactive_energy_kvarh"
     unit = "kVARh"
-    ha_device_class = "energy"
+
+
+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):
diff --git a/xknx/dpt/dpt_string.py b/xknx/dpt/dpt_string.py
index 2d4ae5a4..89a468a7 100644
--- a/xknx/dpt/dpt_string.py
+++ b/xknx/dpt/dpt_string.py
@@ -56,6 +56,7 @@ class DPTLatin1(DPTString):
     DPT 16.001
     """
 
+    dpt_main_number = 16
     dpt_sub_number = 1
     value_type = "latin_1"
     _encoding = "latin_1"
diff --git a/xknx/io/routing.py b/xknx/io/routing.py
index 603f56ea..bf844f2a 100644
--- a/xknx/io/routing.py
+++ b/xknx/io/routing.py
@@ -257,9 +257,8 @@ class Routing(Interface):
 
     def _handle_routing_indication(self, routing_indication: RoutingIndication) -> None:
         """Handle incoming RoutingIndication."""
-        cemi = CEMIFrame()
         try:
-            cemi.from_knx(routing_indication.raw_cemi)
+            cemi = CEMIFrame.from_knx(routing_indication.raw_cemi)
         except UnsupportedCEMIMessage as unsupported_cemi_err:
             logger.warning("CEMI not supported: %s", unsupported_cemi_err)
             return
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 4283f83f..7ca3d2da 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -307,9 +307,8 @@ class _Tunnel(Interface):
         self, tunneling_request: TunnellingRequest
     ) -> None:
         """Handle incoming tunnel request."""
-        cemi = CEMIFrame()
         try:
-            cemi.from_knx(tunneling_request.raw_cemi)
+            cemi = CEMIFrame.from_knx(tunneling_request.raw_cemi)
         except UnsupportedCEMIMessage as unsupported_cemi_err:
             logger.warning("CEMI not supported: %s", unsupported_cemi_err)
             return


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index ed75c22f..b44fa0b6 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -4,7 +4,7 @@ isort==5.11.4
 flake8==6.0.0
 flake8-isort==6.0.0
 pydocstyle==6.2.3
-pylint==2.15.9
+pylint==2.15.10
 pytest==7.2.0
 pytest-asyncio==0.20.3
 pytest-cov==4.0.0
diff --git a/test/cemi_tests/cemi_frame_test.py b/test/cemi_tests/cemi_frame_test.py
index 33f9a502..3abf04a3 100644
--- a/test/cemi_tests/cemi_frame_test.py
+++ b/test/cemi_tests/cemi_frame_test.py
@@ -1,8 +1,7 @@
 """Tests for the CEMIFrame object."""
 import pytest
 
-from xknx.cemi import CEMIFlags, CEMIMessageCode
-from xknx.cemi.cemi_frame import CEMIFrame
+from xknx.cemi import CEMIFlags, CEMIFrame, CEMIMessageCode
 from xknx.exceptions import ConversionError, UnsupportedCEMIMessage
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import GroupValueRead
@@ -32,15 +31,13 @@ def get_data(code, adil, flags, src, dst, npdu_len, tpci_apci, payload):
 def test_valid_command():
     """Test for valid frame parsing."""
     raw = get_data(0x29, 0, 0x0080, 1, 1, 1, 0, [])
-    frame = CEMIFrame()
-    packet_len = frame.from_knx(raw)
+    frame = CEMIFrame.from_knx(raw)
     assert frame.code == CEMIMessageCode.L_DATA_IND
     assert frame.flags == 0x0080
     assert frame.src_addr == IndividualAddress(1)
     assert frame.dst_addr == GroupAddress(1)
     assert frame.payload == GroupValueRead()
     assert frame.tpci == TDataGroup()
-    assert packet_len == 11
     assert frame.calculated_length() == 11
     assert frame.to_knx() == raw
 
@@ -48,15 +45,13 @@ def test_valid_command():
 def test_valid_tpci_control():
     """Test for valid tpci control."""
     raw = bytes((0x29, 0, 0, 0, 0, 0, 0, 0, 0, 0x80))
-    frame = CEMIFrame()
-    packet_len = frame.from_knx(raw)
+    frame = CEMIFrame.from_knx(raw)
     assert frame.code == CEMIMessageCode.L_DATA_IND
     assert frame.flags == 0
     assert frame.payload is None
     assert frame.src_addr == IndividualAddress(0)
     assert frame.dst_addr == IndividualAddress(0)
     assert frame.tpci == TConnect()
-    assert packet_len == 10
     assert frame.calculated_length() == 10
     assert frame.to_knx() == raw
 
@@ -80,52 +75,26 @@ def test_valid_tpci_control():
 )
 def test_invalid_tpci_apci(raw, err_msg):
     """Test for invalid APCIService."""
-    frame = CEMIFrame()
     with pytest.raises(UnsupportedCEMIMessage, match=err_msg):
-        frame.from_knx_data_link_layer(raw)
+        CEMIFrame.from_knx_data_link_layer(raw, code=CEMIMessageCode.L_DATA_IND)
 
 
 def test_invalid_apdu_len():
     """Test for invalid apdu len."""
-    frame = CEMIFrame()
     with pytest.raises(UnsupportedCEMIMessage, match=r".*APDU LEN should be .*"):
-        frame.from_knx(get_data(0x29, 0, 0, 0, 0, 2, 0, []))
-
-
-def test_invalid_src_addr():
-    """Test for invalid src addr."""
-    frame = CEMIFrame()
-    frame.code = CEMIMessageCode.L_DATA_IND
-    frame.flags = 0
-    frame.payload = GroupValueRead()
-    frame.src_addr = GroupAddress(0)
-    frame.dst_addr = IndividualAddress(0)
-
-    with pytest.raises(ConversionError, match=r"src_addr invalid"):
-        frame.to_knx()
-
-
-def test_invalid_dst_addr():
-    """Test for invalid dst addr."""
-    frame = CEMIFrame()
-    frame.code = CEMIMessageCode.L_DATA_IND
-    frame.flags = 0
-    frame.payload = GroupValueRead()
-    frame.src_addr = IndividualAddress(0)
-    frame.dst_addr = None
-
-    with pytest.raises(ConversionError, match=r"dst_addr invalid"):
-        frame.to_knx()
+        CEMIFrame.from_knx(get_data(0x29, 0, 0, 0, 0, 2, 0, []))
 
 
 def test_invalid_payload():
     """Test for having wrong payload set."""
-    frame = CEMIFrame()
-    frame.code = CEMIMessageCode.L_DATA_IND
-    frame.flags = 0
-    frame.payload = None
-    frame.src_addr = IndividualAddress(0)
-    frame.dst_addr = IndividualAddress(0)
+    frame = CEMIFrame(
+        code=CEMIMessageCode.L_DATA_IND,
+        flags=0,
+        src_addr=IndividualAddress(0),
+        dst_addr=IndividualAddress(0),
+        tpci=TDataGroup(),
+        payload=None,
+    )
 
     with pytest.raises(TypeError):
         frame.calculated_length()
@@ -136,53 +105,47 @@ def test_invalid_payload():
 
 def test_from_knx_with_not_handleable_cemi():
     """Test for having unhandlebale cemi set."""
-    frame = CEMIFrame()
     with pytest.raises(
         UnsupportedCEMIMessage, match=r".*CEMIMessageCode not implemented:.*"
     ):
-        frame.from_knx(get_data(0x30, 0, 0, 0, 0, 2, 0, []))
+        CEMIFrame.from_knx(get_data(0x30, 0, 0, 0, 0, 2, 0, []))
 
 
 def test_from_knx_with_not_implemented_cemi():
     """Test for having not implemented CEMI set."""
-    frame = CEMIFrame()
     with pytest.raises(
         UnsupportedCEMIMessage, match=r".*Could not handle CEMIMessageCode:.*"
     ):
-        frame.from_knx(
+        CEMIFrame.from_knx(
             get_data(CEMIMessageCode.L_BUSMON_IND.value, 0, 0, 0, 0, 2, 0, [])
         )
 
 
 def test_invalid_invalid_len():
     """Test for invalid cemi len."""
-    frame = CEMIFrame()
     with pytest.raises(UnsupportedCEMIMessage, match=r".*CEMI too small.*"):
-        frame.from_knx_data_link_layer(get_data(0x29, 0, 0, 0, 0, 2, 0, [])[:5])
+        CEMIFrame.from_knx_data_link_layer(
+            get_data(0x29, 0, 0, 0, 0, 2, 0, [])[:5],
+            code=CEMIMessageCode.L_DATA_IND,
+        )
 
 
 def test_from_knx_group_address():
     """Test conversion for a cemi with a group address as destination."""
-    frame = CEMIFrame()
-    frame.from_knx(get_data(0x29, 0, 0x80, 0, 0, 1, 0, []))
-
+    frame = CEMIFrame.from_knx(get_data(0x29, 0, 0x80, 0, 0, 1, 0, []))
     assert frame.dst_addr == GroupAddress(0)
 
 
 def test_from_knx_individual_address():
     """Test conversion for a cemi with a individual address as destination."""
-    frame = CEMIFrame()
-    frame.from_knx(get_data(0x29, 0, 0x00, 0, 0, 1, 0, []))
-
+    frame = CEMIFrame.from_knx(get_data(0x29, 0, 0x00, 0, 0, 1, 0, []))
     assert frame.dst_addr == IndividualAddress(0)
 
 
 def test_telegram_group_address():
     """Test telegram conversion flags with a group address."""
-    frame = CEMIFrame()
     _telegram = Telegram(destination_address=GroupAddress(1))
-    # test CEMIFrame.telegram setter
-    frame.telegram = _telegram
+    frame = CEMIFrame.init_from_telegram(_telegram)
     assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
     assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_LOW
     # test CEMIFrame.telegram property
@@ -191,10 +154,8 @@ def test_telegram_group_address():
 
 def test_telegram_broadcast():
     """Test telegram conversion flags with a group address."""
-    frame = CEMIFrame()
     _telegram = Telegram(destination_address=GroupAddress(0))
-    # test CEMIFrame.telegram setter
-    frame.telegram = _telegram
+    frame = CEMIFrame.init_from_telegram(_telegram)
     assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
     assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
     assert frame.tpci == TDataBroadcast()
@@ -204,10 +165,8 @@ def test_telegram_broadcast():
 
 def test_telegram_individual_address():
     """Test telegram conversion flags with a individual address."""
-    frame = CEMIFrame()
     _telegram = Telegram(destination_address=IndividualAddress(0), tpci=TConnect())
-    # test CEMIFrame.telegram setter
-    frame.telegram = _telegram
+    frame = CEMIFrame.init_from_telegram(_telegram)
     assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS
     assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
     assert frame.flags & 0x0200 == CEMIFlags.NO_ACK_REQUESTED
@@ -217,6 +176,5 @@ def test_telegram_individual_address():
 
 def test_telegram_unsupported_address():
     """Test telegram conversion flags with an unsupported address."""
-    frame = CEMIFrame()
     with pytest.raises(TypeError):
-        frame.telegram = Telegram(destination_address=object())
+        CEMIFrame.init_from_telegram(Telegram(destination_address=object()))
diff --git a/test/dpt_tests/dpt_1byte_signed_test.py b/test/dpt_tests/dpt_1byte_signed_test.py
index 24396e45..81bdc798 100644
--- a/test/dpt_tests/dpt_1byte_signed_test.py
+++ b/test/dpt_tests/dpt_1byte_signed_test.py
@@ -50,6 +50,6 @@ class TestDPTRelativeValue:
 
     def test_unit(self):
         """Test unit of 1 byte relative value objects."""
-        assert DPTSignedRelativeValue.unit == ""
+        assert DPTSignedRelativeValue.unit is None
         assert DPTPercentV8.unit == "%"
         assert DPTValue1Count.unit == "counter pulses"
diff --git a/test/dpt_tests/dpt_4bit_control_test.py b/test/dpt_tests/dpt_4bit_control_test.py
index 60a29763..29e6ebf0 100644
--- a/test/dpt_tests/dpt_4bit_control_test.py
+++ b/test/dpt_tests/dpt_4bit_control_test.py
@@ -70,7 +70,7 @@ class TestDPTControlStepCode:
 
     def test_unit(self):
         """Test unit_of_measurement function."""
-        assert DPTControlStepCode.unit == ""
+        assert DPTControlStepCode.unit is None
 
 
 class TestDPTControlStepwise:
diff --git a/test/dpt_tests/dpt_float_test.py b/test/dpt_tests/dpt_float_test.py
index 630e7a68..211a21aa 100644
--- a/test/dpt_tests/dpt_float_test.py
+++ b/test/dpt_tests/dpt_float_test.py
@@ -195,7 +195,7 @@ class TestDPTFloat:
     # DPTAirFlow
     #
     def test_airflow_settings(self):
-        """Test attributes of DPTPartsPerMillion."""
+        """Test attributes of DPTAirFlow."""
         assert DPTAirFlow.unit == "m³/h"
 
     #
@@ -229,7 +229,7 @@ class TestDPTFloat:
         """Test DPT4ByteFloat object."""
         assert DPT4ByteFloat.from_knx((0x3F, 0x71, 0xEB, 0x86)) == 0.9450001
         assert DPT4ByteFloat.to_knx(0.945000052452) == (0x3F, 0x71, 0xEB, 0x86)
-        assert DPT4ByteFloat.unit == ""
+        assert DPT4ByteFloat.unit is None
 
     def test_4byte_float_values_from_voltage_meter(self):
         """Test parsing DPT4ByteFloat from voltage meter."""
diff --git a/test/dpt_tests/dpt_lookup_test.py b/test/dpt_tests/dpt_lookup_test.py
new file mode 100644
index 00000000..23b3d084
--- /dev/null
+++ b/test/dpt_tests/dpt_lookup_test.py
@@ -0,0 +1,199 @@
+"""Test DPT lookup."""
+# flake8: noqa
+import pytest
+
+from xknx.dpt import *
+
+# """
+# Generate a list of all DPTs for the following test.
+# Run this function manually to update the list.
+# """
+
+# dpt_dict = {}
+# for dpt in DPTBase.dpt_class_tree():
+#     if dpt.value_type is not None:
+#         dpt_dict[dpt.value_type] = dpt
+
+# for _, dpt in sorted(dpt_dict.items()):
+#     unit = None
+#     if dpt.unit is not None:
+#         unit = f'"{dpt.unit}"'
+#     print(
+#         f'("{dpt.value_type}", {dpt.__name__}, {dpt.dpt_main_number}, {dpt.dpt_sub_number}, {unit}),'
+#     )
+
+
+@pytest.mark.parametrize(
+    "value_type,dpt_class,main,sub,unit",
+    [
+        ("1byte_signed", DPTSignedRelativeValue, 6, None, None),
+        ("1byte_unsigned", DPTValue1ByteUnsigned, 5, None, None),
+        ("2byte_float", DPT2ByteFloat, 9, None, None),
+        ("2byte_signed", DPT2ByteSigned, 8, None, None),
+        ("2byte_unsigned", DPT2ByteUnsigned, 7, None, None),
+        ("4byte_float", DPT4ByteFloat, 14, None, None),
+        ("4byte_signed", DPT4ByteSigned, 13, None, None),
+        ("4byte_unsigned", DPT4ByteUnsigned, 12, None, None),
+        ("absolute_humidity", DPTAbsoluteHumidity, 9, 29, "g/m³"),
+        ("absolute_temperature", DPTAbsoluteTemperature, 14, 69, "K"),
+        ("acceleration", DPTAcceleration, 14, 0, "m/s²"),
+        ("acceleration_angular", DPTAccelerationAngular, 14, 1, "rad/s²"),
+        ("activation_energy", DPTActivationEnergy, 14, 2, "J/mol"),
+        ("active_energy", DPTActiveEnergy, 13, 10, "Wh"),
+        ("active_energy_kwh", DPTActiveEnergykWh, 13, 13, "kWh"),
+        ("active_energy_mwh", DPTActiveEnergyMWh, 13, 16, "MWh"),
+        ("activity", DPTActivity, 14, 3, "s⁻¹"),
+        ("air_flow", DPTAirFlow, 9, 9, "m³/h"),
+        ("amplitude", DPTAmplitude, 14, 5, None),
+        ("angle", DPTAngle, 5, 3, "°"),
+        ("angle_deg", DPTAngleDeg, 14, 7, "°"),
+        ("angle_rad", DPTAngleRad, 14, 6, "rad"),
+        ("angular_frequency", DPTAngularFrequency, 14, 34, "rad/s"),
+        ("angular_momentum", DPTAngularMomentum, 14, 8, "J s"),
+        ("angular_velocity", DPTAngularVelocity, 14, 9, "rad/s"),
+        ("apparant_energy", DPTApparantEnergy, 13, 11, "VAh"),
+        ("apparant_energy_kvah", DPTApparantEnergykVAh, 13, 14, "kVAh"),
+        ("apparent_power", DPTApparentPower, 14, 80, "VA"),
+        ("area", DPTArea, 14, 10, "m²"),
+        ("brightness", DPTBrightness, 7, 13, "lx"),
+        ("capacitance", DPTCapacitance, 14, 11, "F"),
+        ("charge_density_surface", DPTChargeDensitySurface, 14, 12, "C/m²"),
+        ("charge_density_volume", DPTChargeDensityVolume, 14, 13, "C/m³"),
+        ("color_temperature", DPTColorTemperature, 7, 600, "K"),
+        ("common_temperature", DPTCommonTemperature, 14, 68, "°C"),
+        ("compressibility", DPTCompressibility, 14, 14, "m²/N"),
+        ("concentration_ugm3", DPTConcentrationUGM3, 9, 30, "μg/m³"),
+        ("conductance", DPTConductance, 14, 15, "S"),
+        ("counter_pulses", DPTValue1Count, 6, 10, "counter pulses"),
+        ("curr", DPTCurrent, 9, 21, "mA"),
+        ("current", DPTUElCurrentmA, 7, 12, "mA"),
+        ("decimal_factor", DPTDecimalFactor, 5, 5, None),
+        ("delta_time_100ms", DPTDeltaTime100Msec, 8, 4, "ms"),
+        ("delta_time_10ms", DPTDeltaTime10Msec, 8, 3, "ms"),
+        ("delta_time_hrs", DPTDeltaTimeHrs, 8, 7, "h"),
+        ("delta_time_min", DPTDeltaTimeMin, 8, 6, "min"),
+        ("delta_time_ms", DPTDeltaTimeMsec, 8, 2, "ms"),
+        ("delta_time_sec", DPTDeltaTimeSec, 8, 5, "s"),
+        ("density", DPTDensity, 14, 17, "kg/m³"),
+        ("electric_charge", DPTElectricCharge, 14, 18, "C"),
+        ("electric_current", DPTElectricCurrent, 14, 19, "A"),
+        ("electric_current_density", DPTElectricCurrentDensity, 14, 20, "A/m²"),
+        ("electric_dipole_moment", DPTElectricDipoleMoment, 14, 21, "C m"),
+        ("electric_displacement", DPTElectricDisplacement, 14, 22, "C/m²"),
+        ("electric_field_strength", DPTElectricFieldStrength, 14, 23, "V/m"),
+        ("electric_flux", DPTElectricFlux, 14, 24, "c"),
+        ("electric_flux_density", DPTElectricFluxDensity, 14, 25, "C/m²"),
+        ("electric_polarization", DPTElectricPolarization, 14, 26, "C/m²"),
+        ("electric_potential", DPTElectricPotential, 14, 27, "V"),
+        ("electric_potential_difference", DPTElectricPotentialDifference, 14, 28, "V"),
+        ("electrical_conductivity", DPTElectricalConductivity, 14, 16, "S/m"),
+        ("electromagnetic_moment", DPTElectromagneticMoment, 14, 29, "A m²"),
+        ("electromotive_force", DPTElectromotiveForce, 14, 30, "V"),
+        ("energy", DPTEnergy, 14, 31, "J"),
+        ("enthalpy", DPTEnthalpy, 9, 999, "H"),
+        ("flow_rate_m3h", DPTFlowRateM3H, 13, 2, "m³/h"),
+        ("force", DPTForce, 14, 32, "N"),
+        ("frequency", DPTFrequency, 14, 33, "Hz"),
+        ("heat_quantity", DPTHeatQuantity, 14, 37, "J"),
+        ("heatcapacity", DPTHeatCapacity, 14, 35, "J/K"),
+        ("heatflowrate", DPTHeatFlowRate, 14, 36, "W"),
+        ("humidity", DPTHumidity, 9, 7, "%"),
+        ("illuminance", DPTLux, 9, 4, "lx"),
+        ("impedance", DPTImpedance, 14, 38, "Ω"),
+        ("kelvin_per_percent", DPTKelvinPerPercent, 9, 23, "K/%"),
+        ("latin_1", DPTLatin1, 16, 1, None),
+        ("length", DPTLength, 14, 39, "m"),
+        ("length_m", DPTLengthM, 8, 12, "m"),
+        ("length_mm", DPTLengthMm, 7, 11, "mm"),
+        ("light_quantity", DPTLightQuantity, 14, 40, "lm s"),
+        ("long_delta_timesec", DPTLongDeltaTimeSec, 13, 100, "s"),
+        ("long_time_period_hrs", DPTLongTimePeriodHrs, 12, 102, "h"),
+        ("long_time_period_min", DPTLongTimePeriodMin, 12, 101, "min"),
+        ("long_time_period_sec", DPTLongTimePeriodSec, 12, 100, "s"),
+        ("luminance", DPTLuminance, 14, 41, "cd/m²"),
+        ("luminous_flux", DPTLuminousFlux, 14, 42, "lm"),
+        ("luminous_intensity", DPTLuminousIntensity, 14, 43, "cd"),
+        ("magnetic_field_strength", DPTMagneticFieldStrength, 14, 44, "A/m"),
+        ("magnetic_flux", DPTMagneticFlux, 14, 45, "Wb"),
+        ("magnetic_flux_density", DPTMagneticFluxDensity, 14, 46, "T"),
+        ("magnetic_moment", DPTMagneticMoment, 14, 47, "A m²"),
+        ("magnetic_polarization", DPTMagneticPolarization, 14, 48, "T"),
+        ("magnetization", DPTMagnetization, 14, 49, "A/m"),
+        ("magnetomotive_force", DPTMagnetomotiveForce, 14, 50, "A"),
+        ("mass", DPTMass, 14, 51, "kg"),
+        ("mass_flux", DPTMassFlux, 14, 52, "kg/s"),
+        ("mol", DPTMol, 14, 4, "mol"),
+        ("momentum", DPTMomentum, 14, 53, "N/s"),
+        ("percent", DPTScaling, 5, 1, "%"),
+        ("percentU8", DPTPercentU8, 5, 4, "%"),
+        ("percentV16", DPTPercentV16, 8, 10, "%"),
+        ("percentV8", DPTPercentV8, 6, 1, "%"),
+        ("phaseangledeg", DPTPhaseAngleDeg, 14, 55, "°"),
+        ("phaseanglerad", DPTPhaseAngleRad, 14, 54, "rad"),
+        ("power", DPTPower, 14, 56, "W"),
+        ("power_2byte", DPTPower2Byte, 9, 24, "kW"),
+        ("power_density", DPTPowerDensity, 9, 22, "W/m²"),
+        ("powerfactor", DPTPowerFactor, 14, 57, None),
+        ("ppm", DPTPartsPerMillion, 9, 8, "ppm"),
+        ("pressure", DPTPressure, 14, 58, "Pa"),
+        ("pressure_2byte", DPTPressure2Byte, 9, 6, "Pa"),
+        ("prop_data_type", DPTPropDataType, 7, 10, None),
+        ("pulse", DPTValue1Ucount, 5, 10, "counter pulses"),
+        ("pulse_2byte", DPT2Ucount, 7, 1, "pulses"),
+        ("pulse_2byte_signed", DPTValue2Count, 8, 1, "pulses"),
+        ("pulse_4_ucount", DPTValue4Ucount, 12, 1, "counter pulses"),
+        ("pulse_4byte", DPTValue4Count, 13, 1, "counter pulses"),
+        ("rain_amount", DPTRainAmount, 9, 26, "L/m²"),
+        ("reactance", DPTReactance, 14, 59, "Ω"),
+        ("reactive_energy", DPTReactiveEnergy, 13, 12, "VARh"),
+        ("reactive_energy_kvarh", DPTReactiveEnergykVARh, 13, 15, "kVARh"),
+        ("resistance", DPTResistance, 14, 60, "Ω"),
+        ("resistivity", DPTResistivity, 14, 61, "Ωm"),
+        ("rotation_angle", DPTRotationAngle, 8, 11, "°"),
+        ("scene_number", DPTSceneNumber, 17, 1, None),
+        ("self_inductance", DPTSelfInductance, 14, 62, "H"),
+        ("solid_angle", DPTSolidAngle, 14, 63, "sr"),
+        ("sound_intensity", DPTSoundIntensity, 14, 64, "W/m²"),
+        ("speed", DPTSpeed, 14, 65, "m/s"),
+        ("stepwise_blinds", DPTControlStepwiseBlinds, 3, 8, "%"),
+        ("stepwise_dimming", DPTControlStepwiseDimming, 3, 7, "%"),
+        ("stress", DPTStress, 14, 66, "Pa"),
+        ("string", DPTString, 16, 0, None),
+        ("surface_tension", DPTSurfaceTension, 14, 67, "N/m"),
+        ("tariff", DPTTariff, 5, 6, None),
+        ("temperature", DPTTemperature, 9, 1, "°C"),
+        ("temperature_a", DPTTemperatureA, 9, 3, "K/h"),
+        ("temperature_difference", DPTTemperatureDifference, 14, 70, "K"),
+        ("temperature_difference_2byte", DPTTemperatureDifference2Byte, 9, 2, "K"),
+        ("temperature_f", DPTTemperatureF, 9, 27, "°F"),
+        ("thermal_capacity", DPTThermalCapacity, 14, 71, "J/K"),
+        ("thermal_conductivity", DPTThermalConductivity, 14, 72, "W/mK"),
+        ("thermoelectric_power", DPTThermoelectricPower, 14, 73, "V/K"),
+        ("time_1", DPTTime1, 9, 10, "s"),
+        ("time_2", DPTTime2, 9, 11, "ms"),
+        ("time_period_100msec", DPTTimePeriod100Msec, 7, 4, "ms"),
+        ("time_period_10msec", DPTTimePeriod10Msec, 7, 3, "ms"),
+        ("time_period_hrs", DPTTimePeriodHrs, 7, 7, "h"),
+        ("time_period_min", DPTTimePeriodMin, 7, 6, "min"),
+        ("time_period_msec", DPTTimePeriodMsec, 7, 2, "ms"),
+        ("time_period_sec", DPTTimePeriodSec, 7, 5, "s"),
+        ("time_seconds", DPTTimeSeconds, 14, 74, "s"),
+        ("torque", DPTTorque, 14, 75, "Nm"),
+        ("voltage", DPTVoltage, 9, 20, "mV"),
+        ("volume", DPTVolume, 14, 76, "m³"),
+        ("volume_flow", DPTVolumeFlow, 9, 25, "L/h"),
+        ("volume_flux", DPTVolumeFlux, 14, 77, "m³/s"),
+        ("volume_liquid_litre", DPTVolumeLiquidLitre, 12, 1200, "L"),
+        ("volume_m3", DPTVolumeM3, 12, 1201, "m³"),
+        ("weight", DPTWeight, 14, 78, "N"),
+        ("wind_speed_kmh", DPTWspKmh, 9, 28, "km/h"),
+        ("wind_speed_ms", DPTWsp, 9, 5, "m/s"),
+        ("work", DPTWork, 14, 79, "J"),
+    ],
+)
+def test_dpt_lookup(value_type, dpt_class, main, sub, unit):
+    """Test DPT4ByteUnsigned."""
+    dpt = DPTBase.parse_transcoder(value_type)
+    assert dpt is dpt_class
+    assert DPTBase.transcoder_by_dpt(dpt_main=main, dpt_sub=sub) is dpt_class
+    assert dpt.unit == unit
diff --git a/test/io_tests/routing_test.py b/test/io_tests/routing_test.py
index 571cde41..9f404ca4 100644
--- a/test/io_tests/routing_test.py
+++ b/test/io_tests/routing_test.py
@@ -41,8 +41,7 @@ class TestRouting:
         # L_Data.ind T_Connect from 1.0.250 to 1.0.255 (xknx tunnel endpoint)
         # communication_channel_id: 0x02   sequence_counter: 0x81
         raw_ind = bytes.fromhex("0610 0530 0010 2900b06010fa10ff0080")
-        _cemi = CEMIFrame()
-        _cemi.from_knx(raw_ind[6:])
+        _cemi = CEMIFrame.from_knx(raw_ind[6:])
         test_telegram = _cemi.telegram
         test_telegram.direction = TelegramDirection.INCOMING
 
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
index 2819c310..819e4ba4 100644
--- a/test/io_tests/tunnel_test.py
+++ b/test/io_tests/tunnel_test.py
@@ -72,8 +72,7 @@ class TestUDPTunnel:
     @patch("xknx.io.UDPTunnel._send_tunnelling_ack")
     async def test_tunnel_request_received(self, send_ack_mock, raw):
         """Test Tunnel for calling send_ack on normal frames."""
-        test_cemi = CEMIFrame()
-        test_cemi.from_knx(raw[10:])
+        test_cemi = CEMIFrame.from_knx(raw[10:])
         self.tunnel.expected_sequence_number = 0x21
 
         self.tunnel.transport.data_received_callback(raw, ("192.168.1.2", 3671))
@@ -97,8 +96,7 @@ class TestUDPTunnel:
         # communication_channel_id: 0x02   sequence_counter: 0x81
         raw_ind = bytes.fromhex("0610 0420 0014 04 02 81 00 2900b06010fa10ff0080")
 
-        test_cemi = CEMIFrame()
-        test_cemi.from_knx(raw_ind[10:])
+        test_cemi = CEMIFrame.from_knx(raw_ind[10:])
         test_telegram = test_cemi.telegram
         test_telegram.direction = TelegramDirection.INCOMING
         self.tunnel.expected_sequence_number = 0x81
diff --git a/test/knxip_tests/routing_indication_test.py b/test/knxip_tests/routing_indication_test.py
index 35b5f5a6..04126f2f 100644
--- a/test/knxip_tests/routing_indication_test.py
+++ b/test/knxip_tests/routing_indication_test.py
@@ -37,8 +37,10 @@ class TestKNXIPRountingIndication:
                 DPTArray(DPTTime().to_knx(time.strptime("13:23:42", "%H:%M:%S")))
             ),
         )
-        cemi = CEMIFrame(src_addr=IndividualAddress("1.2.2"))
-        cemi.telegram = telegram
+        cemi = CEMIFrame.init_from_telegram(
+            telegram,
+            src_addr=IndividualAddress("1.2.2"),
+        )
         cemi.set_hops(5)
         routing_indication = RoutingIndication(raw_cemi=cemi.to_knx())
         knxipframe = KNXIPFrame.init_from_body(routing_indication)
diff --git a/test/knxip_tests/tunnelling_request_test.py b/test/knxip_tests/tunnelling_request_test.py
index 220a529a..1d155585 100644
--- a/test/knxip_tests/tunnelling_request_test.py
+++ b/test/knxip_tests/tunnelling_request_test.py
@@ -24,17 +24,18 @@ class TestKNXIPTunnellingRequest:
         assert knxipframe.body.sequence_counter == 23
         assert isinstance(knxipframe.body.raw_cemi, bytes)
 
-        incoming_cemi = CEMIFrame()
-        incoming_cemi.from_knx(knxipframe.body.raw_cemi)
+        incoming_cemi = CEMIFrame.from_knx(knxipframe.body.raw_cemi)
         assert incoming_cemi.telegram == Telegram(
             destination_address=GroupAddress("9/0/8"),
             payload=GroupValueWrite(DPTBinary(1)),
         )
 
-        outgoing_cemi = CEMIFrame(code=CEMIMessageCode.L_DATA_REQ)
-        outgoing_cemi.telegram = Telegram(
-            destination_address=GroupAddress("9/0/8"),
-            payload=GroupValueWrite(DPTBinary(1)),
+        outgoing_cemi = CEMIFrame.init_from_telegram(
+            Telegram(
+                destination_address=GroupAddress("9/0/8"),
+                payload=GroupValueWrite(DPTBinary(1)),
+            ),
+            code=CEMIMessageCode.L_DATA_REQ,
         )
         tunnelling_request = TunnellingRequest(
             communication_channel_id=1,
diff --git a/test/str_test.py b/test/str_test.py
index 8214fdf2..3e9af0e0 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -641,11 +641,12 @@ class TestStringRepresentations:
 
     def test_cemi_frame(self):
         """Test string representation of KNX/IP CEMI Frame."""
-        cemi_frame = CEMIFrame()
-        cemi_frame.src_addr = IndividualAddress("1.2.3")
-        cemi_frame.telegram = Telegram(
-            destination_address=GroupAddress("1/2/5"),
-            payload=GroupValueWrite(DPTBinary(7)),
+        cemi_frame = CEMIFrame.init_from_telegram(
+            Telegram(
+                destination_address=GroupAddress("1/2/5"),
+                payload=GroupValueWrite(DPTBinary(7)),
+            ),
+            src_addr=IndividualAddress("1.2.3"),
         )
         assert (
             str(cemi_frame)