Repository: XKNX/xknx
Pull request #1687 (2025-05-22 10:40:10)

Use `repr()` for values in exceptions
<!--
  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.
-->
to differentiate between eg. integers and numeric strings or bool and `"True"`


Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] 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/1687?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 97.32%. Comparing base [(`5d27516`)](https://app.codecov.io/gh/XKNX/xknx/commit/5d2751658b62c4f9f709870fa85e1d18cdb4e37d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`4a7834b`)](https://app.codecov.io/gh/XKNX/xknx/commit/4a7834b4660ab59030318b40aa8ec8fc7c02a9d7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX).

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/XKNX/xknx/pull/1687/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/1687?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1687   +/-   ##
=======================================
  Coverage   97.32%   97.32%           
=======================================
  Files         160      160           
  Lines       10749    10749           
=======================================
  Hits        10462    10462           
  Misses        287      287           
```

| [Files with missing lines](https://app.codecov.io/gh/XKNX/xknx/pull/1687?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/devices/climate\_mode.py](https://app.codecov.io/gh/XKNX/xknx/pull/1687?src=pr&el=tree&filepath=xknx%2Fdevices%2Fclimate_mode.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2NsaW1hdGVfbW9kZS5weQ==) | `100.00% <ø> (ø)` | |
| [xknx/exceptions/exception.py](https://app.codecov.io/gh/XKNX/xknx/pull/1687?src=pr&el=tree&filepath=xknx%2Fexceptions%2Fexception.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9leGNlcHRpb25zL2V4Y2VwdGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/remote\_value/remote\_value\_step.py](https://app.codecov.io/gh/XKNX/xknx/pull/1687?src=pr&el=tree&filepath=xknx%2Fremote_value%2Fremote_value_step.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9yZW1vdGVfdmFsdWUvcmVtb3RlX3ZhbHVlX3N0ZXAucHk=) | `100.00% <ø> (ø)` | |
| [xknx/remote\_value/remote\_value\_switch.py](https://app.codecov.io/gh/XKNX/xknx/pull/1687?src=pr&el=tree&filepath=xknx%2Fremote_value%2Fremote_value_switch.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9yZW1vdGVfdmFsdWUvcmVtb3RlX3ZhbHVlX3N3aXRjaC5weQ==) | `100.00% <ø> (ø)` | |
| [xknx/remote\_value/remote\_value\_updown.py](https://app.codecov.io/gh/XKNX/xknx/pull/1687?src=pr&el=tree&filepath=xknx%2Fremote_value%2Fremote_value_updown.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9yZW1vdGVfdmFsdWUvcmVtb3RlX3ZhbHVlX3VwZG93bi5weQ==) | `100.00% <ø> (ø)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 96faf18e..4f000a91 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,12 @@ nav_order: 2
 
 # Changelog
 
+# Unreleased changes
+
+### Internal
+
+- Use `repr()` for values in exceptions.
+
 # 3.8.0 Valid energy 2025-05-12
 
 ### Connection
diff --git a/xknx/devices/climate_mode.py b/xknx/devices/climate_mode.py
index cff36845..4c010637 100644
--- a/xknx/devices/climate_mode.py
+++ b/xknx/devices/climate_mode.py
@@ -229,7 +229,7 @@ class ClimateMode(Device):
             or operation_mode not in self._operation_modes
         ):
             raise DeviceIllegalValue(
-                "operation (preset) mode not supported", str(operation_mode)
+                "operation (preset) mode not supported", operation_mode
             )
 
         for rv in self._iter_remote_values():
@@ -245,7 +245,7 @@ class ClimateMode(Device):
             or controller_mode not in self._controller_modes
         ):
             raise DeviceIllegalValue(
-                "controller (HVAC) mode not supported", str(controller_mode)
+                "controller (HVAC) mode not supported", controller_mode
             )
 
         for rv in self._iter_remote_values():
diff --git a/xknx/exceptions/exception.py b/xknx/exceptions/exception.py
index c8efb9dd..a68f79ec 100644
--- a/xknx/exceptions/exception.py
+++ b/xknx/exceptions/exception.py
@@ -55,7 +55,7 @@ class CouldNotParseTelegram(XKNXException):
 
     def _format_parameter(self) -> str:
         return " ".join(
-            [f'{key}="{value}"' for (key, value) in sorted(self.parameter.items())]
+            [f"{key}={value!r}" for (key, value) in sorted(self.parameter.items())]
         )
 
     def __str__(self) -> str:
@@ -137,7 +137,7 @@ class ConversionError(XKNXException):
 
     def _format_parameter(self) -> str:
         return " ".join(
-            [f'{key}="{value}"' for (key, value) in sorted(self.parameter.items())]
+            [f"{key}={value!r}" for (key, value) in sorted(self.parameter.items())]
         )
 
     def __str__(self) -> str:
@@ -157,13 +157,13 @@ class CouldNotParseAddress(XKNXException):
     def __str__(self) -> str:
         """Return object as readable string."""
         _msg = f'message="{self.message}" ' if self.message else ""
-        return f'<CouldNotParseAddress address="{self.address}" {_msg}/>'
+        return f"<CouldNotParseAddress address={self.address!r} {_msg}/>"
 
 
 class DeviceIllegalValue(XKNXException):
     """Exception class for setting a value of a device with an illegal value."""
 
-    def __init__(self, value: Any, description: str) -> None:
+    def __init__(self, description: str, value: Any) -> None:
         """Initialize DeviceIllegalValue class."""
         super().__init__()
         self.value = value
@@ -171,7 +171,7 @@ class DeviceIllegalValue(XKNXException):
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return f'<DeviceIllegalValue description="{self.value}" value="{self.description}" />'
+        return f'<DeviceIllegalValue description="{self.description}" value={self.value!r} />'
 
 
 class DataSecureError(XKNXException):
diff --git a/xknx/remote_value/remote_value_step.py b/xknx/remote_value/remote_value_step.py
index 71a19955..988972be 100644
--- a/xknx/remote_value/remote_value_step.py
+++ b/xknx/remote_value/remote_value_step.py
@@ -73,7 +73,7 @@ class RemoteValueStep(RemoteValue["RemoteValueStep.Direction"]):
             return self.Direction.INCREASE if self.invert else self.Direction.DECREASE
         raise CouldNotParseTelegram(
             "Payload invalid",
-            payload=payload,
+            payload=str(payload),
             device_name=self.device_name,
             feature_name=self.feature_name,
         )
diff --git a/xknx/remote_value/remote_value_switch.py b/xknx/remote_value/remote_value_switch.py
index 61877d29..54b8504f 100644
--- a/xknx/remote_value/remote_value_switch.py
+++ b/xknx/remote_value/remote_value_switch.py
@@ -62,7 +62,7 @@ class RemoteValueSwitch(RemoteValue[bool]):
             return not self.invert
         raise CouldNotParseTelegram(
             "Payload invalid",
-            payload=payload,
+            payload=str(payload),
             device_name=self.device_name,
             feature_name=self.feature_name,
         )
diff --git a/xknx/remote_value/remote_value_updown.py b/xknx/remote_value/remote_value_updown.py
index bf433985..17ed64dd 100644
--- a/xknx/remote_value/remote_value_updown.py
+++ b/xknx/remote_value/remote_value_updown.py
@@ -69,7 +69,7 @@ class RemoteValueUpDown(RemoteValue["RemoteValueUpDown.Direction"]):
             return self.Direction.UP if self.invert else self.Direction.DOWN
         raise CouldNotParseTelegram(
             "Payload invalid",
-            payload=payload,
+            payload=str(payload),
             device_name=self.device_name,
             feature_name=self.feature_name,
         )


Test patch:
diff --git a/test/io_tests/knxip_interface_test.py b/test/io_tests/knxip_interface_test.py
index 6251be53..16fd9741 100644
--- a/test/io_tests/knxip_interface_test.py
+++ b/test/io_tests/knxip_interface_test.py
@@ -682,11 +682,11 @@ class TestKNXIPInterface:
         """Test starting a secure routing connection with a pre-loaded keyring."""
         backbone_key = bytes.fromhex("cf89fd0f18f4889783c7ef44ee1f5e14")
         explicit_keyring = await load_keyring(
-            path=self.knxkeys_file,
-            password="password")
+            path=self.knxkeys_file, password="password"
+        )
         connection_config = ConnectionConfig(
             connection_type=ConnectionType.ROUTING_SECURE,
-            secure_config=SecureConfig(keyring=explicit_keyring)
+            secure_config=SecureConfig(keyring=explicit_keyring),
         )
         with patch("xknx.io.routing.SecureRouting.connect") as connect_secure:
             interface = knx_interface_factory(self.xknx, connection_config)
diff --git a/test/str_test.py b/test/str_test.py
index c04dff91..2536775d 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -413,7 +413,7 @@ class TestStringRepresentations:
         exception = CouldNotParseTelegram(description="Fnord", one="one", two="two")
         assert (
             str(exception)
-            == '<CouldNotParseTelegram description="Fnord" one="one" two="two"/>'
+            == "<CouldNotParseTelegram description=\"Fnord\" one='one' two='two'/>"
         )
 
     def test_could_not_parse_knxip_exception(self) -> None:
@@ -431,20 +431,20 @@ class TestStringRepresentations:
         exception = ConversionError(description="Fnord", one="one", two="two")
         assert (
             str(exception)
-            == '<ConversionError description="Fnord" one="one" two="two"/>'
+            == "<ConversionError description=\"Fnord\" one='one' two='two'/>"
         )
 
     def test_could_not_parse_address_exception(self) -> None:
         """Test string representation of CouldNotParseAddress exception."""
         exception = CouldNotParseAddress(address="1/2/1000")
-        assert str(exception) == '<CouldNotParseAddress address="1/2/1000" />'
+        assert str(exception) == "<CouldNotParseAddress address='1/2/1000' />"
 
     def test_device_illegal_value_exception(self) -> None:
         """Test string representation of DeviceIllegalValue exception."""
         exception = DeviceIllegalValue(value=12, description="Fnord exceeded")
         assert (
             str(exception)
-            == '<DeviceIllegalValue description="12" value="Fnord exceeded" />'
+            == '<DeviceIllegalValue description="Fnord exceeded" value=12 />'
         )
 
     def test_incomplete_knxip_frame_excetpion(self) -> None: