Repository: XKNX/xknx
Pull request #1186 (2023-02-08 10:02:59)

Bump cryptography from 39.0.0 to 39.0.1
Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.0 to 39.0.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst">cryptography's changelog</a>.</em></p>
<blockquote>
<p>39.0.1 - 2023-02-07</p>
<pre><code>
* **SECURITY ISSUE** - Fixed a bug where ``Cipher.update_into`` accepted Python
  buffer protocol objects, but allowed immutable buffers. **CVE-2023-23931**
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL 3.0.8.
<p>.. _v39-0-0:
</code></pre></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pyca/cryptography/commit/d6951dca25de45abd52da51b608055371fbcde4e"><code>d6951dc</code></a> changelog + security fix backport (<a href="https://github-redirect.dependabot.com/pyca/cryptography/issues/8231">#8231</a>)</li>
<li><a href="https://github.com/pyca/cryptography/commit/138da90c8450446b19619e3faa77b9da54c34be3"><code>138da90</code></a> workaround scapy bug in downstream tests (<a href="https://github-redirect.dependabot.com/pyca/cryptography/issues/8218">#8218</a>) (<a href="https://github-redirect.dependabot.com/pyca/cryptography/issues/8228">#8228</a>)</li>
<li><a href="https://github.com/pyca/cryptography/commit/69527bc79095c9646d7e839121f0783477892ecc"><code>69527bc</code></a> bookworm is py311 now (<a href="https://github-redirect.dependabot.com/pyca/cryptography/issues/8200">#8200</a>)</li>
<li><a href="https://github.com/pyca/cryptography/commit/111deefb659b8d73c56d3ce89458f2df973d60e4"><code>111deef</code></a> backport main branch CI to 39.0.x (<a href="https://github-redirect.dependabot.com/pyca/cryptography/issues/8153">#8153</a>)</li>
<li>See full diff in <a href="https://github.com/pyca/cryptography/compare/39.0.0...39.0.1">compare view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/1186?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1186](https://codecov.io/gh/XKNX/xknx/pull/1186?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (8609195) into [main](https://codecov.io/gh/XKNX/xknx/commit/aa557cdc451d30cca9b6666990a0e9a32bb8e2c5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (aa557cd) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1186/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/1186?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1186   +/-   ##
=======================================
  Coverage   96.43%   96.43%           
=======================================
  Files         143      143           
  Lines        8900     8900           
=======================================
  Hits         8583     8583           
  Misses        317      317           
```




Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index a88744f0..08288319 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -11,12 +11,18 @@ nav_order: 2
 ### Connection
 
 - Use only Interfaces listed in Keyring when `ConnectionType.AUTOMATIC` is used and a Keyring is configured.
+- Request specific tunnel by individual address for TCP connections when `ConnectionConfig.individual_address` is set.
 
 ### Bugfixes
 
 - Parse Data Secure credentials form Keyring from non-IP-Secure interfaces.
 - Parse Data Secure credentials from Keyrings exported for specific interfaces.
 
+### Protocol
+
+- Support Extended Connection Request Information (CRI) for requesting a specific individual address on Tunnelling v2.
+- Add Core v2 Error Code definitions.
+
 ### Cleanups
 
 - Accept `str | os.PathLike` for Keyring path. Previously only `str`.
diff --git a/docs/xknx.md b/docs/xknx.md
index 8e995450..92e9f98a 100644
--- a/docs/xknx.md
+++ b/docs/xknx.md
@@ -73,6 +73,8 @@ An explicit connection configuration can be used. In this case a `connection_typ
 KNX Data Secure credentials are sourced from a keyfile exported from ETS.
 IP Secure keys can be configured directly or sourced from a keyfile.
 
+For TCP tunnel connections a specific tunnel endpoint can be requested by setting `individual_address`. For SECURE tunnels this setting selects an interface from a given keyfile.
+
 # [](#header-2)Starting
 
 ```python
diff --git a/requirements/production.txt b/requirements/production.txt
index ee008d14..69360bbf 100644
--- a/requirements/production.txt
+++ b/requirements/production.txt
@@ -1,2 +1,2 @@
-cryptography==39.0.0
+cryptography==39.0.1
 ifaddr==0.2.0
diff --git a/xknx/dpt/dpt_2byte_float.py b/xknx/dpt/dpt_2byte_float.py
index 0ba86340..ba3e39d4 100644
--- a/xknx/dpt/dpt_2byte_float.py
+++ b/xknx/dpt/dpt_2byte_float.py
@@ -132,6 +132,7 @@ class DPTWsp(DPT2ByteFloat):
     dpt_sub_number = 5
     value_type = "wind_speed_ms"
     unit = "m/s"
+    ha_device_class = "wind_speed"
 
     value_min = 0
     value_max = 670760
@@ -212,6 +213,7 @@ class DPTVoltage(DPT2ByteFloat):
     dpt_sub_number = 20
     value_type = "voltage"
     unit = "mV"
+    ha_device_class = "voltage"
 
 
 class DPTCurrent(DPT2ByteFloat):
@@ -221,6 +223,7 @@ class DPTCurrent(DPT2ByteFloat):
     dpt_sub_number = 21
     value_type = "curr"
     unit = "mA"
+    ha_device_class = "current"
 
 
 class DPTPowerDensity(DPT2ByteFloat):
@@ -292,6 +295,7 @@ class DPTWspKmh(DPT2ByteFloat):
     dpt_sub_number = 28
     value_type = "wind_speed_kmh"
     unit = "km/h"
+    ha_device_class = "wind_speed"
 
     value_min = 0
     value_max = 670760
diff --git a/xknx/dpt/dpt_2byte_signed.py b/xknx/dpt/dpt_2byte_signed.py
index 0e2ef741..a27cf39c 100644
--- a/xknx/dpt/dpt_2byte_signed.py
+++ b/xknx/dpt/dpt_2byte_signed.py
@@ -146,3 +146,4 @@ class DPTLengthM(DPT2ByteSigned):
     dpt_sub_number = 12
     value_type = "length_m"
     unit = "m"
+    ha_device_class = "distance"
diff --git a/xknx/dpt/dpt_2byte_uint.py b/xknx/dpt/dpt_2byte_uint.py
index b8a6a2ba..b6e39958 100644
--- a/xknx/dpt/dpt_2byte_uint.py
+++ b/xknx/dpt/dpt_2byte_uint.py
@@ -125,6 +125,7 @@ class DPTLengthMm(DPT2ByteUnsigned):
     dpt_sub_number = 11
     value_type = "length_mm"
     unit = "mm"
+    ha_device_class = "distance"
 
 
 class DPTUElCurrentmA(DPT2ByteUnsigned):
@@ -134,6 +135,7 @@ class DPTUElCurrentmA(DPT2ByteUnsigned):
     dpt_sub_number = 12
     value_type = "current"
     unit = "mA"
+    ha_device_class = "current"
 
 
 class DPTBrightness(DPT2ByteUnsigned):
@@ -143,6 +145,7 @@ class DPTBrightness(DPT2ByteUnsigned):
     dpt_sub_number = 13
     value_type = "brightness"
     unit = "lx"
+    ha_device_class = "illuminance"
 
 
 class DPTColorTemperature(DPT2ByteUnsigned):
diff --git a/xknx/dpt/dpt_4byte_float.py b/xknx/dpt/dpt_4byte_float.py
index ed2b6c6c..9649a7d3 100644
--- a/xknx/dpt/dpt_4byte_float.py
+++ b/xknx/dpt/dpt_4byte_float.py
@@ -239,6 +239,7 @@ class DPTElectricCurrent(DPT4ByteFloat):
     dpt_sub_number = 19
     value_type = "electric_current"
     unit = "A"
+    ha_device_class = "current"
 
 
 class DPTElectricCurrentDensity(DPT4ByteFloat):
@@ -365,6 +366,7 @@ class DPTFrequency(DPT4ByteFloat):
     dpt_sub_number = 33
     value_type = "frequency"
     unit = "Hz"
+    ha_device_class = "frequency"
 
 
 class DPTAngularFrequency(DPT4ByteFloat):
@@ -419,6 +421,7 @@ class DPTLength(DPT4ByteFloat):
     dpt_sub_number = 39
     value_type = "length"
     unit = "m"
+    ha_device_class = "distance"
 
 
 class DPTLightQuantity(DPT4ByteFloat):
@@ -437,7 +440,6 @@ class DPTLuminance(DPT4ByteFloat):
     dpt_sub_number = 41
     value_type = "luminance"
     unit = "cd/m²"
-    ha_device_class = "illuminance"
 
 
 class DPTLuminousFlux(DPT4ByteFloat):
@@ -447,7 +449,6 @@ class DPTLuminousFlux(DPT4ByteFloat):
     dpt_sub_number = 42
     value_type = "luminous_flux"
     unit = "lm"
-    ha_device_class = "illuminance"
 
 
 class DPTLuminousIntensity(DPT4ByteFloat):
@@ -457,7 +458,6 @@ class DPTLuminousIntensity(DPT4ByteFloat):
     dpt_sub_number = 43
     value_type = "luminous_intensity"
     unit = "cd"
-    ha_device_class = "illuminance"
 
 
 class DPTMagneticFieldStrength(DPT4ByteFloat):
@@ -530,6 +530,7 @@ class DPTMass(DPT4ByteFloat):
     dpt_sub_number = 51
     value_type = "mass"
     unit = "kg"
+    ha_device_class = "weight"
 
 
 class DPTMassFlux(DPT4ByteFloat):
@@ -658,6 +659,7 @@ class DPTSpeed(DPT4ByteFloat):
     dpt_sub_number = 65
     value_type = "speed"
     unit = "m/s"
+    ha_device_class = "speed"
 
 
 class DPTStress(DPT4ByteFloat):
@@ -793,3 +795,4 @@ class DPTApparentPower(DPT4ByteFloat):
     dpt_sub_number = 80
     value_type = "apparent_power"
     unit = "VA"
+    ha_device_class = "apparent_power"
diff --git a/xknx/io/connection.py b/xknx/io/connection.py
index 7cd0bf8f..e01becc9 100644
--- a/xknx/io/connection.py
+++ b/xknx/io/connection.py
@@ -34,6 +34,7 @@ class ConnectionConfig:
         * TUNNELING_TCP connect to a specific KNX/IP tunneling v2 device via TCP.
     * individual address:
         * ROUTING the individual address used as source address for routing
+        * TCP TUNNELING request a specific tunnel endpoint
         * SECURE TUNNELING use a specific tunnel endpoint from the knxkeys file
     * local_ip: Local ip of the interface though which KNXIPInterface should connect.
     * gateway_ip: IP of KNX/IP tunneling device.
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index c04eb054..5e0ec5eb 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -184,15 +184,21 @@ class KNXIPInterface:
     ) -> None:
         """Start KNX/IP TCP tunnel."""
         util.validate_ip(gateway_ip, address_name="Gateway IP address")
+        tunnel_address = self.connection_config.individual_address
+
         logger.debug(
-            "Starting tunnel to %s:%s over TCP",
+            "Starting tunnel to %s:%s over TCP%s",
             gateway_ip,
             gateway_port,
+            f" requesting individual address {tunnel_address}"
+            if tunnel_address
+            else "",
         )
         self._interface = TCPTunnel(
             self.xknx,
             gateway_ip=gateway_ip,
             gateway_port=gateway_port,
+            individual_address=tunnel_address,
             cemi_received_callback=self.cemi_received,
             auto_reconnect=self.connection_config.auto_reconnect,
             auto_reconnect_wait=self.connection_config.auto_reconnect_wait,
diff --git a/xknx/io/request_response/connect.py b/xknx/io/request_response/connect.py
index 90f7dfd9..355d41d1 100644
--- a/xknx/io/request_response/connect.py
+++ b/xknx/io/request_response/connect.py
@@ -6,10 +6,11 @@ from typing import TYPE_CHECKING
 from xknx.knxip import (
     HPAI,
     ConnectRequest,
-    ConnectRequestType,
+    ConnectRequestInformation,
     ConnectResponse,
     KNXIPFrame,
 )
+from xknx.telegram import IndividualAddress
 
 from .request_response import RequestResponse
 
@@ -18,27 +19,34 @@ if TYPE_CHECKING:
 
 
 class Connect(RequestResponse):
-    """Class to send a ConnectRequest and wait for ConnectResponse."""
+    """
+    Class to send a ConnectRequest and wait for ConnectResponse.
+
+    Setting a `individual_address` is only supported for Tunnelling v2 connections.
+    """
 
     def __init__(
         self,
         transport: KNXIPTransport,
         local_hpai: HPAI,
+        individual_address: IndividualAddress | None = None,
     ):
         """Initialize Connect class."""
         super().__init__(transport, ConnectResponse)
         self.communication_channel = 0
         self.data_endpoint = HPAI()
         self.identifier = 0
+        self.individual_address = individual_address
         self.local_hpai = local_hpai
 
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
+        cri = ConnectRequestInformation(individual_address=self.individual_address)
         # use the same HPAI for control_endpoint and data_endpoint
         connect_request = ConnectRequest(
-            request_type=ConnectRequestType.TUNNEL_CONNECTION,
             control_endpoint=self.local_hpai,
             data_endpoint=self.local_hpai,
+            cri=cri,
         )
         return KNXIPFrame.init_from_body(connect_request)
 
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 6925e620..285867e0 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -70,6 +70,7 @@ class _Tunnel(Interface):
         self._initial_connection = True
         self._is_reconnecting = False
         self._reconnect_task: asyncio.Task[None] | None = None
+        self._requested_address: IndividualAddress | None = None
         self._src_address = IndividualAddress(0)
         self._send_lock = asyncio.Lock()
         # self._tunnelling_request_confirmation_event = asyncio.Event()
@@ -186,7 +187,11 @@ class _Tunnel(Interface):
 
     async def _connect_request(self) -> bool:
         """Connect to tunnelling server. Set communication_channel and src_address."""
-        connect = Connect(transport=self.transport, local_hpai=self.local_hpai)
+        connect = Connect(
+            transport=self.transport,
+            local_hpai=self.local_hpai,
+            individual_address=self._requested_address,
+        )
         await connect.start()
         if connect.success:
             self.communication_channel = connect.communication_channel
@@ -557,6 +562,7 @@ class TCPTunnel(_Tunnel):
         cemi_received_callback: CEMICallbackType,
         gateway_ip: str,
         gateway_port: int,
+        individual_address: IndividualAddress | None = None,
         auto_reconnect: bool = True,
         auto_reconnect_wait: int = 3,
     ):
@@ -571,6 +577,7 @@ class TCPTunnel(_Tunnel):
         )
         # TCP always uses 0.0.0.0:0
         self.local_hpai = HPAI(protocol=HostProtocol.IPV4_TCP)
+        self._requested_address = individual_address
 
     def _init_transport(self) -> None:
         """Initialize transport transport."""
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index c92f44f5..07b0e121 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -1,7 +1,7 @@
 """This package contains all methods for serialization and deserialization of KNX/IP packets."""
 # flake8: noqa
 from .body import KNXIPBody, KNXIPBodyResponse
-from .connect_request import ConnectRequest
+from .connect_request import ConnectRequest, ConnectRequestInformation
 from .connect_response import ConnectResponse
 from .connectionstate_request import ConnectionStateRequest
 from .connectionstate_response import ConnectionStateResponse
@@ -29,6 +29,7 @@ from .knxip_enum import (
     KNXIPServiceType,
     KNXMedium,
     SearchRequestParameterType,
+    TunnellingLayer,
 )
 from .routing_busy import RoutingBusy
 from .routing_indication import RoutingIndication
@@ -51,6 +52,7 @@ __all__ = [
     "KNXIPBody",
     "KNXIPBodyResponse",
     "ConnectRequest",
+    "ConnectRequestInformation",
     "ConnectResponse",
     "ConnectionStateRequest",
     "ConnectionStateResponse",
@@ -90,5 +92,6 @@ __all__ = [
     "SRP",
     "TimerNotify",
     "TunnellingAck",
+    "TunnellingLayer",
     "TunnellingRequest",
 ]
diff --git a/xknx/knxip/connect_request.py b/xknx/knxip/connect_request.py
index 234fa76a..f8e56344 100644
--- a/xknx/knxip/connect_request.py
+++ b/xknx/knxip/connect_request.py
@@ -6,69 +6,46 @@ Connect requests are used to start a new tunnel connection on a KNX/IP device.
 from __future__ import annotations
 
 from xknx.exceptions import CouldNotParseKNXIP
+from xknx.telegram import IndividualAddress
 
 from .body import KNXIPBody
 from .hpai import HPAI
-from .knxip_enum import ConnectRequestType, KNXIPServiceType
+from .knxip_enum import ConnectRequestType, KNXIPServiceType, TunnellingLayer
 
 
 class ConnectRequest(KNXIPBody):
     """Representation of a KNX Connect Request."""
 
     SERVICE_TYPE = KNXIPServiceType.CONNECT_REQUEST
-    CRI_LENGTH = 4
 
     def __init__(
         self,
-        request_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
         control_endpoint: HPAI | None = None,
         data_endpoint: HPAI | None = None,
+        cri: ConnectRequestInformation | None = None,
     ):
         """Initialize ConnectRequest object."""
-        self.request_type = request_type
         self.control_endpoint = control_endpoint or HPAI()
         self.data_endpoint = data_endpoint or HPAI()
-        # KNX layer, 0x02 = TUNNEL_LINKLAYER
-        self.flags = 0x02
+        self.cri = cri or ConnectRequestInformation()
 
     def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
-        return HPAI.LENGTH + HPAI.LENGTH + ConnectRequest.CRI_LENGTH
+        return HPAI.LENGTH + HPAI.LENGTH + self.cri.calculated_length()
 
     def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
-
-        def cri_from_knx(cri: bytes) -> int:
-            """Parse CRI (Connection Request Information)."""
-            if cri[0] != ConnectRequest.CRI_LENGTH:
-                raise CouldNotParseKNXIP("CRI has wrong length")
-            if len(cri) < ConnectRequest.CRI_LENGTH:
-                raise CouldNotParseKNXIP("CRI data has wrong length")
-            self.request_type = ConnectRequestType(cri[1])
-            self.flags = cri[2]
-            return 4
-
         pos = self.control_endpoint.from_knx(raw)
         pos += self.data_endpoint.from_knx(raw[pos:])
-        pos += cri_from_knx(raw[pos:])
+        pos += self.cri.from_knx(raw[pos:])
         return pos
 
     def to_knx(self) -> bytes:
         """Serialize to KNX/IP raw data."""
-
-        def cri_to_knx() -> bytes:
-            """Serialize CRI (Connect Request Information)."""
-            return bytes(
-                (
-                    ConnectRequest.CRI_LENGTH,
-                    self.request_type.value,
-                    self.flags,
-                    0x00,  # Reserved
-                )
-            )
-
         return (
-            self.control_endpoint.to_knx() + self.data_endpoint.to_knx() + cri_to_knx()
+            self.control_endpoint.to_knx()
+            + self.data_endpoint.to_knx()
+            + self.cri.to_knx()
         )
 
     def __repr__(self) -> str:
@@ -77,6 +54,86 @@ class ConnectRequest(KNXIPBody):
             "<ConnectRequest "
             f'control_endpoint="{self.control_endpoint}" '
             f'data_endpoint="{self.data_endpoint}" '
-            f'request_type="{self.request_type}" '
-            f'flags="{hex(self.flags)}" />'
+            f'cri="{self.cri}" />'
+        )
+
+
+class ConnectRequestInformation:
+    """
+    Representation of a KNX Connect Request Information (CRI).
+
+    A Basic CRI requests a tunnel without requesting any specific IA.
+    Using `individual_address` yields an Extended CRI which is only
+    supported by Tunnelling v2 devices.
+    """
+
+    CRI_BASIC_LENGTH = 4
+    CRI_EXTENDED_LENGTH = 6
+
+    def __init__(
+        self,
+        connection_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
+        knx_layer: TunnellingLayer = TunnellingLayer.DATA_LINK_LAYER,
+        individual_address: IndividualAddress | None = None,
+    ):
+        """Initialize ConnectRequest object."""
+        self.connection_type = connection_type
+        self.knx_layer = knx_layer
+        self.individual_address = individual_address
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        return (
+            ConnectRequestInformation.CRI_EXTENDED_LENGTH
+            if self.individual_address
+            else ConnectRequestInformation.CRI_BASIC_LENGTH
+        )
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        cri_length = raw[0]
+        if cri_length == ConnectRequestInformation.CRI_BASIC_LENGTH:
+            extended = False
+        elif cri_length == ConnectRequestInformation.CRI_EXTENDED_LENGTH:
+            extended = True
+        else:
+            raise CouldNotParseKNXIP("CRI has wrong length")
+        if len(raw) < cri_length:
+            raise CouldNotParseKNXIP("CRI data has wrong length")
+        self.connection_type = ConnectRequestType(raw[1])
+        self.knx_layer = TunnellingLayer(raw[2])
+        if extended:
+            self.individual_address = IndividualAddress.from_knx(raw[4:6])
+        return cri_length
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        _cri = bytes(
+            (
+                self.calculated_length(),
+                self.connection_type.value,
+                self.knx_layer.value,
+                0x00,  # Reserved
+            )
+        )
+        if self.individual_address is None:
+            return _cri
+        return _cri + self.individual_address.to_knx()
+
+    def __eq__(self, other: object) -> bool:
+        """Equal operator."""
+        return self.__dict__ == other.__dict__
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        _extended = (
+            f'individual_address="{self.individual_address}" '
+            if self.individual_address
+            else ""
+        )
+        return (
+            "<ConnectRequestInformation "
+            f'connection_type="{self.connection_type.name}" '
+            f'knx_layer="{self.knx_layer.name}" '
+            f"{_extended}/>"
         )
diff --git a/xknx/knxip/error_code.py b/xknx/knxip/error_code.py
index 68444ce3..349b4f2a 100644
--- a/xknx/knxip/error_code.py
+++ b/xknx/knxip/error_code.py
@@ -17,6 +17,10 @@ class ErrorCode(Enum):
     # received sequence number is out of order.
     E_SEQUENCE_NUMBER = 0x04
 
+    # Any further undefined, possibly implementation specific error has occurred.
+    # Core v2
+    E_ERROR = 0x0F
+
     # The KNXnet/IP Server device cannot find an active data
     # connection with the specified ID.
     E_CONNECTION_ID = 0x21
@@ -44,6 +48,18 @@ class ErrorCode(Enum):
     # the KNX subnetwork connection with the specified ID.
     E_KNX_CONNECTION = 0x27
 
+    # The Client is not authorised to use the requested IA in the Extended CRI.
+    # Core v2
+    E_AUTHORISATION_ERROR = 0x28
+
     # The requested tunnelling layer is not supported by the
     # KNXnet/IP Server device.
     E_TUNNELLING_LAYER = 0x29
+
+    # The IA requested in the Extended CRI is not a Tunnelling IA.
+    # Core v2
+    E_NO_TUNNELLING_ADDRESS = 0x2D
+
+    # The IA requested for this connection is in use.
+    # Core v2
+    E_CONNECTION_IN_USE = 0x2E
diff --git a/xknx/knxip/knxip_enum.py b/xknx/knxip/knxip_enum.py
index ac5eac32..0ab5e94d 100644
--- a/xknx/knxip/knxip_enum.py
+++ b/xknx/knxip/knxip_enum.py
@@ -54,47 +54,48 @@ class ConnectRequestType(Enum):
 
     # Data connection used to configure a KNXnet/IP device
     DEVICE_MGMT_CONNECTION = 0x03
-
     # Data connection used to forward KNX telegrams between
     # two KNXnet/IP devices.
     TUNNEL_CONNECTION = 0x04
-
     # Data connection used for configuration and data transfer
     # with a remote logging server.
     REMLOG_CONNECTION = 0x06
-
     # Data connection used for data transfer with a remote
     # configuration server.
     REMCONF_CONNECTION = 0x07
-
     # Data connection used for configuration and data transfer
     # with an Object Server in a KNXnet/IP device.
     OBJSVR_CONNECTION = 0x08
 
 
+class TunnellingLayer(Enum):
+    """Enum class for KNX/IP Tunnelling Layer."""
+
+    # Data Link Layer tunnel
+    DATA_LINK_LAYER = 0x02
+    # Raw tunnel
+    RAW_LAYER = 0x04
+    # Busmonitor tunnel
+    BUSMONITOR_LAYER = 0x80
+
+
 class DIBTypeCode(Enum):
     """Enum class for KNX/IP DIB Type Codes."""
 
     # Device information e.g. KNX medium.
     DEVICE_INFO = 0x01
-
     # Service families supported by the device.
     SUPP_SVC_FAMILIES = 0x02
-
     # IP configuration
     IP_CONFIG = 0x03
-
     # current configuration
     IP_CUR_CONFIG = 0x04
-
     # KNX addresses
     KNX_ADDRESSES = 0x05
-
     # KNX IP Secure
     SECURED_SERVICE_FAMILIES = 0x06
     TUNNELING_INFO = 0x07
     ADDITIONAL_DEVICE_INFO = 0x08
-
     # DIB structure for further data defined by device manufacturer.
     MFR_DATA = 0xFE
 
@@ -120,25 +121,18 @@ class DIBServiceFamily(Enum):
 
     #  Core
     CORE = 0x02
-
     # Device Management
     DEVICE_MANAGEMENT = 0x03
-
     # Tunnelling
     TUNNELING = 0x04
-
     # Routing
     ROUTING = 0x05
-
     # Remote Logging
     REMOTE_LOGGING = 0x06
-
     # Configuration and Diagnosis
     REMOTE_CONFIGURATION_DIAGNOSIS = 0x07
-
     # Object Server'.
     OBJECT_SERVER = 0x08
-
     # Security - Extended search response only
     SECURITY = 0x09
 
@@ -165,15 +159,11 @@ class SearchRequestParameterType(Enum):
 
     # Used to test behavior of the KNXnet/IP server for unknown SRPs. Don't use!
     INVALID = 0x00
-
     # Select only KNXnet/IP Servers that are currently in programming mode
     SELECT_BY_PROGRAMMING_MODE = 0x01
-
     # Select only KNXnet/IP Servers that have the given MAC address
     SELECT_BY_MAC_ADDRESS = 0x02
-
     # Select only KNXnet/IP Servers that support the given DIBServiceFamily in a given version
     SELECT_BY_SERVICE = 0x03
-
     # The Client shall include this SRP to indicate that it is interested in the listed DIBs
     REQUEST_DIBS = 0x04


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 8fc47d53..9b3e6599 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -9,5 +9,5 @@ pytest==7.2.1
 pytest-asyncio==0.20.3
 pytest-cov==4.0.0
 setuptools==67.2.0
-tox==4.4.4
+tox==4.4.5
 mypy==1.0.0
diff --git a/test/devices_tests/weather_test.py b/test/devices_tests/weather_test.py
index cc409bb0..df80b9c8 100644
--- a/test/devices_tests/weather_test.py
+++ b/test/devices_tests/weather_test.py
@@ -134,7 +134,7 @@ class TestWeather:
 
         assert weather.wind_speed == 469237.76
         assert weather._wind_speed.unit_of_measurement == "m/s"
-        assert weather._wind_speed.ha_device_class is None
+        assert weather._wind_speed.ha_device_class == "wind_speed"
 
     async def test_wind_bearing(self):
         """Test wind bearing received."""
diff --git a/test/io_tests/knxip_interface_test.py b/test/io_tests/knxip_interface_test.py
index c1e0dc20..0b3bb322 100644
--- a/test/io_tests/knxip_interface_test.py
+++ b/test/io_tests/knxip_interface_test.py
@@ -181,6 +181,38 @@ class TestKNXIPInterface:
             )
             connect_tcp.assert_called_once_with()
 
+    async def test_start_tcp_tunnel_connection_with_ia(self):
+        """Test starting TCP tunnel connection requesting specific tunnel."""
+        # without gateway_ip automatic is called
+        gateway_ip = "127.0.0.2"
+        connection_config = ConnectionConfig(
+            connection_type=ConnectionType.TUNNELING_TCP,
+            gateway_ip=gateway_ip,
+            individual_address="1.1.1",
+        )
+        with patch(
+            "xknx.io.KNXIPInterface._start_tunnelling_tcp"
+        ) as start_tunnelling_tcp:
+            interface = knx_interface_factory(self.xknx, connection_config)
+            await interface.start()
+            start_tunnelling_tcp.assert_called_once_with(
+                gateway_ip=gateway_ip,
+                gateway_port=3671,
+            )
+        with patch("xknx.io.tunnel.TCPTunnel.connect") as connect_tcp:
+            interface = knx_interface_factory(self.xknx, connection_config)
+            await interface.start()
+            assert isinstance(interface._interface, TCPTunnel)
+            assert interface._interface.gateway_ip == gateway_ip
+            assert interface._interface.gateway_port == 3671
+            assert interface._interface._requested_address == IndividualAddress("1.1.1")
+            assert interface._interface.auto_reconnect
+            assert interface._interface.auto_reconnect_wait == 3
+            assert (  # pylint: disable=comparison-with-callable
+                interface._interface.cemi_received_callback == interface.cemi_received
+            )
+            connect_tcp.assert_called_once_with()
+
     async def test_start_routing_connection(self):
         """Test starting routing connection."""
         local_ip = "127.0.0.1"
diff --git a/test/io_tests/request_response_tests/connect_test.py b/test/io_tests/request_response_tests/connect_test.py
index f76cf6f5..6c77b373 100644
--- a/test/io_tests/request_response_tests/connect_test.py
+++ b/test/io_tests/request_response_tests/connect_test.py
@@ -7,7 +7,6 @@ from xknx.knxip import (
     HPAI,
     ConnectionStateRequest,
     ConnectRequest,
-    ConnectRequestType,
     ConnectResponse,
     ErrorCode,
     KNXIPFrame,
@@ -29,7 +28,6 @@ class TestConnect:
         # Expected KNX/IP-Frame:
         exp_knxipframe = KNXIPFrame.init_from_body(
             ConnectRequest(
-                request_type=ConnectRequestType.TUNNEL_CONNECTION,
                 control_endpoint=local_hpai,
                 data_endpoint=local_hpai,
             )
@@ -83,9 +81,7 @@ class TestConnect:
         assert connect.awaited_response_class == ConnectResponse
 
         # Expected KNX/IP-Frame:
-        exp_knxipframe = KNXIPFrame.init_from_body(
-            ConnectRequest(request_type=ConnectRequestType.TUNNEL_CONNECTION)
-        )
+        exp_knxipframe = KNXIPFrame.init_from_body(ConnectRequest())
         with patch("xknx.io.transport.UDPTransport.send") as mock_udp_send, patch(
             "xknx.io.transport.UDPTransport.getsockname"
         ) as mock_udp_getsockname:
diff --git a/test/knxip_tests/connect_request_test.py b/test/knxip_tests/connect_request_test.py
index 12ffca75..13db0894 100644
--- a/test/knxip_tests/connect_request_test.py
+++ b/test/knxip_tests/connect_request_test.py
@@ -2,7 +2,15 @@
 import pytest
 
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import HPAI, ConnectRequest, ConnectRequestType, KNXIPFrame
+from xknx.knxip import (
+    HPAI,
+    ConnectRequest,
+    ConnectRequestInformation,
+    ConnectRequestType,
+    KNXIPFrame,
+    TunnellingLayer,
+)
+from xknx.telegram import IndividualAddress
 
 
 class TestKNXIPConnectRequest:
@@ -17,16 +25,58 @@ class TestKNXIPConnectRequest:
         knxipframe, _ = KNXIPFrame.from_knx(raw)
 
         assert isinstance(knxipframe.body, ConnectRequest)
-        assert knxipframe.body.request_type == ConnectRequestType.TUNNEL_CONNECTION
         assert knxipframe.body.control_endpoint == HPAI(
             ip_addr="192.168.42.1", port=33941
         )
         assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.1", port=52393)
+        assert (
+            knxipframe.body.cri.connection_type is ConnectRequestType.TUNNEL_CONNECTION
+        )
+        assert knxipframe.body.cri.knx_layer is TunnellingLayer.DATA_LINK_LAYER
+        assert knxipframe.body.cri.individual_address is None
+
+        cri = ConnectRequestInformation(
+            connection_type=ConnectRequestType.TUNNEL_CONNECTION,
+            knx_layer=TunnellingLayer.DATA_LINK_LAYER,
+            individual_address=None,
+        )
+        connect_request = ConnectRequest(
+            control_endpoint=HPAI(ip_addr="192.168.42.1", port=33941),
+            data_endpoint=HPAI(ip_addr="192.168.42.1", port=52393),
+            cri=cri,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_request)
+
+        assert knxipframe2.to_knx() == raw
 
+    def test_connect_request_extended_cri(self):
+        """Test parsing and streaming connection request KNX/IP packet."""
+        raw = bytes.fromhex(
+            "06 10 02 05 00 1C 08 01 C0 A8 2A 01 84 95 08 01"
+            "C0 A8 2A 01 CC A9 06 04 02 00 01 02"
+        )
+        knxipframe, _ = KNXIPFrame.from_knx(raw)
+
+        assert isinstance(knxipframe.body, ConnectRequest)
+        assert knxipframe.body.control_endpoint == HPAI(
+            ip_addr="192.168.42.1", port=33941
+        )
+        assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.1", port=52393)
+        assert (
+            knxipframe.body.cri.connection_type is ConnectRequestType.TUNNEL_CONNECTION
+        )
+        assert knxipframe.body.cri.knx_layer is TunnellingLayer.DATA_LINK_LAYER
+        assert knxipframe.body.cri.individual_address == IndividualAddress("0.1.2")
+
+        cri = ConnectRequestInformation(
+            connection_type=ConnectRequestType.TUNNEL_CONNECTION,
+            knx_layer=TunnellingLayer.DATA_LINK_LAYER,
+            individual_address=IndividualAddress("0.1.2"),
+        )
         connect_request = ConnectRequest(
-            request_type=ConnectRequestType.TUNNEL_CONNECTION,
             control_endpoint=HPAI(ip_addr="192.168.42.1", port=33941),
             data_endpoint=HPAI(ip_addr="192.168.42.1", port=52393),
+            cri=cri,
         )
         knxipframe2 = KNXIPFrame.init_from_body(connect_request)
 
diff --git a/test/str_test.py b/test/str_test.py
index 3e9af0e0..8ad71aec 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -526,7 +526,7 @@ class TestStringRepresentations:
         assert (
             str(connect_request)
             == '<ConnectRequest control_endpoint="192.168.42.1:33941/udp" data_endpoint="192.168.42.2:33942/udp" '
-            'request_type="ConnectRequestType.TUNNEL_CONNECTION" flags="0x2" />'
+            'cri="<ConnectRequestInformation connection_type="TUNNEL_CONNECTION" knx_layer="DATA_LINK_LAYER" />" />'
         )
 
     def test_connect_response(self):