Repository: XKNX/xknx
Pull request #1600 (2024-11-10 07:49:33)

Add `return_code` for TunnellingFeatureResponse
<!--
  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.
-->
TunnellingFeatureResponse was missing `return_code`. 
When an error occured - eg. `ReturnCode.E_ACCESS_READ_ONLY` -  some tunnelling server implementations omit `data` (Gira, Weinzierl) while others return the received data (Enertex).

cc @pdgendt 
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.
-->

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests

## Checklist

- [ ] The documentation has been adjusted accordingly
- [x] 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/1600?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
Attention: Patch coverage is `94.28571%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 96.92%. Comparing base [(`e98b284`)](https://app.codecov.io/gh/XKNX/xknx/commit/e98b2844fc0364bf35c01bfc4d8344e0b4f0bf7c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`8fc9f71`)](https://app.codecov.io/gh/XKNX/xknx/commit/8fc9f71a915e998db6e4f75fce0acb2f358a33cb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX).

| [Files with missing lines](https://app.codecov.io/gh/XKNX/xknx/pull/1600?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Patch % | Lines |
|---|---|---|
| [xknx/knxip/tunnelling\_feature.py](https://app.codecov.io/gh/XKNX/xknx/pull/1600?src=pr&el=tree&filepath=xknx%2Fknxip%2Ftunnelling_feature.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC90dW5uZWxsaW5nX2ZlYXR1cmUucHk=) | 89.47% | [2 Missing :warning: ](https://app.codecov.io/gh/XKNX/xknx/pull/1600?src=pr&el=tree&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/1600/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/1600?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@            Coverage Diff             @@
##             main    #1600      +/-   ##
==========================================
- Coverage   96.92%   96.92%   -0.01%     
==========================================
  Files         157      158       +1     
  Lines       10582    10617      +35     
==========================================
+ Hits        10257    10290      +33     
- Misses        325      327       +2     
```

| [Files with missing lines](https://app.codecov.io/gh/XKNX/xknx/pull/1600?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/management/application\_layer\_enum.py](https://app.codecov.io/gh/XKNX/xknx/pull/1600?src=pr&el=tree&filepath=xknx%2Fmanagement%2Fapplication_layer_enum.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9tYW5hZ2VtZW50L2FwcGxpY2F0aW9uX2xheWVyX2VudW0ucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/knxip/tunnelling\_feature.py](https://app.codecov.io/gh/XKNX/xknx/pull/1600?src=pr&el=tree&filepath=xknx%2Fknxip%2Ftunnelling_feature.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC90dW5uZWxsaW5nX2ZlYXR1cmUucHk=) | `97.18% <89.47%> (-2.82%)` | :arrow_down: |

</details>

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 661fce5c..03333483 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,8 +6,11 @@ nav_order: 2
 
 # Changelog
 
-- Added rate limit (in packets per second) option to P2PConnection.
+### Management
+
+- Add rate limit (in packets per second) option to P2PConnection.
 - Fix typo in management procedure (`nm_invididual_address_write` was renamed to `nm_individual_address_write`)
+- Fix TunnellingFeatureResponse missing `return_code`
 
 # 3.3.0 Climate humidity 2024-10-20
 
diff --git a/xknx/knxip/tunnelling_feature.py b/xknx/knxip/tunnelling_feature.py
index b7938208..30438b22 100644
--- a/xknx/knxip/tunnelling_feature.py
+++ b/xknx/knxip/tunnelling_feature.py
@@ -13,6 +13,7 @@ from __future__ import annotations
 import struct
 
 from xknx.exceptions import CouldNotParseKNXIP
+from xknx.management.application_layer_enum import ReturnCode
 
 from .body import KNXIPBody, KNXIPBodyResponse
 from .error_code import ErrorCode
@@ -162,6 +163,53 @@ class TunnellingFeatureResponse(_TunnellingFeature, KNXIPBodyResponse):
 
     SERVICE_TYPE = KNXIPServiceType.TUNNELLING_FEATURE_RESPONSE
 
+    def __init__(
+        self,
+        communication_channel_id: int = 1,
+        sequence_counter: int = 0,
+        feature_type: TunnellingFeatureType = TunnellingFeatureType.SUPPORTED_EMI_TYPE,
+        return_code: ReturnCode = ReturnCode.E_SUCCESS,
+        data: bytes = b"",
+    ):
+        """Initialize TunnellingFeatureSet object."""
+        super().__init__(
+            communication_channel_id=communication_channel_id,
+            sequence_counter=sequence_counter,
+            feature_type=feature_type,
+            data=data,
+        )
+        self.return_code = return_code
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        if raw[0] != _TunnellingFeature.HEADER_LENGTH:  # structure_length field
+            raise CouldNotParseKNXIP("TunnellingFeature header has invalid length")
+        self.communication_channel_id = raw[1]
+        self.sequence_counter = raw[2]
+        self.status_code = ErrorCode(raw[3])
+        self.feature_type = TunnellingFeatureType(raw[4])
+        self.return_code = ReturnCode(raw[5])
+        self.data = raw[6:]
+        if self.return_code is ReturnCode.E_SUCCESS and len(self.data) == 0:
+            # Data may be omitted by some servers when an error occurred
+            raise CouldNotParseKNXIP("TunnellingFeature missing data.")
+        return len(raw)
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        data_size = len(self.data) + (len(self.data) % 2) if self._has_data() else 0
+        return struct.pack(
+            # Append a NUL byte if data size is uneven
+            f"!BBBBBB{data_size}s",
+            _TunnellingFeature.HEADER_LENGTH,
+            self.communication_channel_id,
+            self.sequence_counter,
+            self.status_code.value,
+            self.feature_type.value,
+            self.return_code.value,
+            self.data,
+        )
+
     def __repr__(self) -> str:
         """Return object as readable string."""
         return (
@@ -170,5 +218,6 @@ class TunnellingFeatureResponse(_TunnellingFeature, KNXIPBodyResponse):
             f'sequence_counter="{self.sequence_counter}" '
             f'status_code="{self.status_code}" '
             f'feature_type="{self.feature_type}" '
+            f'return_code="{self.return_code}" '
             f'data="{self.data.hex()}" />'
         )
diff --git a/xknx/management/application_layer_enum.py b/xknx/management/application_layer_enum.py
new file mode 100644
index 00000000..70eb87d2
--- /dev/null
+++ b/xknx/management/application_layer_enum.py
@@ -0,0 +1,46 @@
+"""Enums for KNX Application Layer."""
+
+from enum import Enum
+
+
+class ReturnCode(Enum):
+    """Enum class for Generic device management Return Codes."""
+
+    ## Basic positive Return Code
+    # The service, function or command is executed successfully, without additional information.
+    E_SUCCESS = 0x00
+    ## Generic negative Return Codes
+    # Memory cannot be accessed or only with fault(s).
+    E_MEMORY_ERROR = 0xF1
+    # Requested data will not fit into a Frame supported by this server.
+    # This shall be used for Device limitations of the maximum supported Frame length
+    # by accessing resources (Properties, Function Properties, memory…) of the device.
+    E_LENGTH_EXCEEDS_MAX_APDU_LENGTH = 0xF4
+    # Writing data beyond what is reserved for the addressed Resource.
+    E_DATA_OVERFLOW = 0xF5
+    # Write value too low. Preferable to give this instead of “Value not supported”.
+    E_DATA_MIN = 0xF6
+    # Write value too high. Preferable to give this instead of “Value not supported”.
+    E_DATA_MAX = 0xF7
+    # The service or function is supported, but request data is not valid for this receiver.
+    E_DATA_VOID = 0xF8
+    # Data could generally be written, but not possible at this time.
+    E_TEMPORARILY_NOT_AVAILABLE = 0xF9
+    # Read access attempted to a “write only” service or Resource.
+    E_ACCESS_WRITE_ONLY = 0xFA
+    # Write access attempted to a “read only” service or Resource.
+    E_ACCESS_READ_ONLY = 0xFB
+    # Access denied due to authorization reasons. A_Authorize as well as KNX Security
+    E_ACCESS_DENIED = 0xFC
+    # Interface Object or Property is not present, or index is out of range.
+    E_ADDRESS_VOID = 0xFD
+    # Write access with a wrong datatype (Datapoint length).
+    E_DATA_TYPE_CONFLICT = 0xFE
+    # The service, function or command has failed without a closer indication of the problem.
+    E_ERROR = 0xFF
+    ## Generic positive Return Codes
+    # (01h-1Fh - None proposed)
+    ## Specific positive Return Codes
+    # (20h-5Fh - None proposed)
+    ## Specific negative Return Codes
+    # (A0h-DFh - None proposed)


Test patch:
diff --git a/test/knxip_tests/tunnelling_feature_test.py b/test/knxip_tests/tunnelling_feature_test.py
index 6ab6e26e..8b02b0bc 100644
--- a/test/knxip_tests/tunnelling_feature_test.py
+++ b/test/knxip_tests/tunnelling_feature_test.py
@@ -12,6 +12,7 @@ from xknx.knxip import (
     TunnellingFeatureSet,
     TunnellingFeatureType,
 )
+from xknx.management.application_layer_enum import ReturnCode
 
 
 class TestKNXIPTunnellingFeature:
@@ -74,13 +75,14 @@ class TestKNXIPTunnellingFeature:
         assert (
             knxipframe.body.feature_type == TunnellingFeatureType.BUS_CONNECTION_STATUS
         )
+        assert knxipframe.body.return_code == ReturnCode.E_SUCCESS
         assert knxipframe.body.data == b"\x01\x00"
 
         tunnelling_request = TunnellingFeatureResponse(
             communication_channel_id=1,
             sequence_counter=23,
-            status_code=ErrorCode.E_NO_ERROR,
             feature_type=TunnellingFeatureType.BUS_CONNECTION_STATUS,
+            return_code=ReturnCode.E_SUCCESS,
             data=b"\x01\x00",
         )
         knxipframe2 = KNXIPFrame.init_from_body(tunnelling_request)
diff --git a/test/str_test.py b/test/str_test.py
index 53150e4a..2bb0fb29 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -695,7 +695,7 @@ class TestStringRepresentations:
         assert (
             str(tunnelling_feature)
             == '<TunnellingFeatureResponse communication_channel_id="23" sequence_counter="42" status_code="ErrorCode.E_NO_ERROR" '
-            'feature_type="TunnellingFeatureType.BUS_CONNECTION_STATUS" data="0100" />'
+            'feature_type="TunnellingFeatureType.BUS_CONNECTION_STATUS" return_code="ReturnCode.E_SUCCESS" data="0100" />'
         )
 
     def test_tunnelling_feature_set(self):