Repository: XKNX/xknx
Pull request #924 (2022-04-20 13:55:53)

Use `__repr__` instead of `__str__` in xknx.knxip
<!--
  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.
-->
- These are perfectly reproduceable now so we can just print them in a better readable way anywhere. 
Additionally its a cheat-move to increase coverage for these package.
- Remove default `mandatory` flag of request_device_description SRP according to Frame sent by ETS

## 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)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options



Patch:
diff --git a/changelog.md b/changelog.md
index 9ac321bb..de38f0e1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,12 @@
 
 ## Unreleased changes
 
+## 0.20.4 Fix exposure of time and date
+
+### Bugfixes
+
+- Fix exposure of datetime, time and date objects to the Bus
+
 ### Protocol
 
 - Add DIBSecuredServiceFamilies and DIBTunnelingInfo parser
diff --git a/xknx/__version__.py b/xknx/__version__.py
index dbc751c2..ad78310e 100644
--- a/xknx/__version__.py
+++ b/xknx/__version__.py
@@ -1,3 +1,3 @@
 """XKNX version."""
 
-__version__ = "0.20.3"
+__version__ = "0.20.4"
diff --git a/xknx/core/task_registry.py b/xknx/core/task_registry.py
index 08eda622..330891d9 100644
--- a/xknx/core/task_registry.py
+++ b/xknx/core/task_registry.py
@@ -23,16 +23,21 @@ class Task:
         name: str,
         task: AsyncCallbackType,
         restart_after_reconnect: bool = False,
+        loop: asyncio.AbstractEventLoop | None = None,
     ) -> None:
         """Initialize Task class."""
         self.name = name
         self.task = task
         self.restart_after_reconnect = restart_after_reconnect
         self._task: asyncio.Task[None] | None = None
+        self._loop = loop
 
     def start(self) -> None:
         """Start a task."""
-        self._task = asyncio.create_task(self.task, name=self.name)
+        if self._loop is not None:
+            self._task = self._loop.create_task(self.task, name=self.name)
+        else:
+            self._task = asyncio.create_task(self.task, name=self.name)
 
     def __await__(self) -> Generator[None, None, None]:
         """Wait for task to be finished."""
@@ -66,9 +71,15 @@ class TaskRegistry:
 
     def __init__(self, xknx: XKNX) -> None:
         """Initialize TaskRegistry class."""
+        self._main_loop: asyncio.AbstractEventLoop | None = None
+
         self.xknx = xknx
         self.tasks: list[Task] = []
 
+    async def register_loop(self) -> None:
+        """Register the main loop for thread safe interaction."""
+        self._main_loop = asyncio.get_running_loop()
+
     def register(
         self,
         name: str,
@@ -80,7 +91,10 @@ class TaskRegistry:
         self.unregister(name)
 
         _task: Task = Task(
-            name=name, task=task, restart_after_reconnect=restart_after_reconnect
+            name=name,
+            task=task,
+            restart_after_reconnect=restart_after_reconnect,
+            loop=self._main_loop,
         )
 
         if track_task:
diff --git a/xknx/knxip/cemi_frame.py b/xknx/knxip/cemi_frame.py
index 1d86231e..2cf91a41 100644
--- a/xknx/knxip/cemi_frame.py
+++ b/xknx/knxip/cemi_frame.py
@@ -194,7 +194,7 @@ class CEMIFrame:
             + self.payload.to_knx()
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<CEMIFrame "
diff --git a/xknx/knxip/connect_request.py b/xknx/knxip/connect_request.py
index a430f82d..812214eb 100644
--- a/xknx/knxip/connect_request.py
+++ b/xknx/knxip/connect_request.py
@@ -71,11 +71,12 @@ class ConnectRequest(KNXIPBody):
             self.control_endpoint.to_knx() + self.data_endpoint.to_knx() + cri_to_knx()
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<ConnectRequest "
             f'control_endpoint="{self.control_endpoint}" '
             f'data_endpoint="{self.data_endpoint}" '
-            f'request_type="{self.request_type}" />'
+            f'request_type="{self.request_type}" '
+            f'flags="{hex(self.flags)}" />'
         )
diff --git a/xknx/knxip/connect_response.py b/xknx/knxip/connect_response.py
index f87de90a..caa08a7f 100644
--- a/xknx/knxip/connect_response.py
+++ b/xknx/knxip/connect_response.py
@@ -83,7 +83,7 @@ class ConnectResponse(KNXIPBodyResponse):
             + crd_to_knx()
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<ConnectResponse "
diff --git a/xknx/knxip/connectionstate_request.py b/xknx/knxip/connectionstate_request.py
index 273398a1..4be958c2 100644
--- a/xknx/knxip/connectionstate_request.py
+++ b/xknx/knxip/connectionstate_request.py
@@ -52,10 +52,10 @@ class ConnectionStateRequest(KNXIPBody):
             + self.control_endpoint.to_knx()
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<ConnectionStateRequest "
-            f'CommunicationChannelID="{self.communication_channel_id}", '
+            f'communication_channel_id="{self.communication_channel_id}", '
             f'control_endpoint="{self.control_endpoint}" />'
         )
diff --git a/xknx/knxip/connectionstate_response.py b/xknx/knxip/connectionstate_response.py
index 44388dc9..a7304be0 100644
--- a/xknx/knxip/connectionstate_response.py
+++ b/xknx/knxip/connectionstate_response.py
@@ -44,10 +44,10 @@ class ConnectionStateResponse(KNXIPBodyResponse):
         """Serialize to KNX/IP raw data."""
         return bytes((self.communication_channel_id, self.status_code.value))
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<ConnectionStateResponse "
-            f'CommunicationChannelID="{self.communication_channel_id}" '
+            f'communication_channel_id="{self.communication_channel_id}" '
             f'status_code="{self.status_code}" />'
         )
diff --git a/xknx/knxip/description_request.py b/xknx/knxip/description_request.py
index cd7d0b41..4f839e21 100644
--- a/xknx/knxip/description_request.py
+++ b/xknx/knxip/description_request.py
@@ -32,6 +32,6 @@ class DescriptionRequest(KNXIPBody):
         """Serialize to KNX/IP raw data."""
         return self.control_endpoint.to_knx()
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return f'<DescriptionRequest control_endpoint="{self.control_endpoint}" />'
diff --git a/xknx/knxip/description_response.py b/xknx/knxip/description_response.py
index 6708adfd..959523ea 100644
--- a/xknx/knxip/description_response.py
+++ b/xknx/knxip/description_response.py
@@ -48,7 +48,7 @@ class DescriptionResponse(KNXIPBody):
         """Serialize to KNX/IP raw data."""
         return b"".join(dib.to_knx() for dib in self.dibs)
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
-        _dibs_str = ",\n".join(dib.__str__() for dib in self.dibs)
+        _dibs_str = ",\n".join(dib.__repr__() for dib in self.dibs)
         return "<DescriptionResponse " f'dibs="[\n{_dibs_str}\n]" />'
diff --git a/xknx/knxip/dib.py b/xknx/knxip/dib.py
index 42c12305..df1867a0 100644
--- a/xknx/knxip/dib.py
+++ b/xknx/knxip/dib.py
@@ -112,7 +112,7 @@ class DIBGeneric(DIB):
             + bytes(len(self.data) % 2)  # padding
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return f'<DIB dtc="{self.dtc}" data="{", ".join(f"0x{i:02x}" for i in self.data)}" />'
 
@@ -197,7 +197,7 @@ class DIBDeviceInformation(DIB):
             + name_str_to_knx(self.name)
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<DIBDeviceInformation "
@@ -230,7 +230,7 @@ class DIBSuppSVCFamilies(DIB):
             """Serialize to KNX/IP raw data."""
             return bytes((self.name.value, self.version))
 
-        def __str__(self) -> str:
+        def __repr__(self) -> str:
             """Return object as readable string."""
             return f'<Family name="{self.name}" version="{self.version}" />'
 
@@ -281,7 +281,7 @@ class DIBSuppSVCFamilies(DIB):
             )
         ) + b"".join(family.to_knx() for family in self.families)
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         _families_str = ", ".join(
             f"{family.name} version: {family.version}" for family in self.families
@@ -360,7 +360,7 @@ class DIBTunnelingInfo(DIB):
             )
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             f"<{self.__class__.__name__} max_adpu_lenght={self.max_apdu_length} "
diff --git a/xknx/knxip/disconnect_request.py b/xknx/knxip/disconnect_request.py
index f4de0bf3..0fea20f9 100644
--- a/xknx/knxip/disconnect_request.py
+++ b/xknx/knxip/disconnect_request.py
@@ -45,10 +45,10 @@ class DisconnectRequest(KNXIPBody):
             + self.control_endpoint.to_knx()
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<DisconnectRequest "
-            f'CommunicationChannelID="{self.communication_channel_id}" '
+            f'communication_channel_id="{self.communication_channel_id}" '
             f'control_endpoint="{self.control_endpoint}" />'
         )
diff --git a/xknx/knxip/disconnect_response.py b/xknx/knxip/disconnect_response.py
index e04ccfe0..c06ca213 100644
--- a/xknx/knxip/disconnect_response.py
+++ b/xknx/knxip/disconnect_response.py
@@ -49,10 +49,10 @@ class DisconnectResponse(KNXIPBodyResponse):
             )
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<DisconnectResponse "
-            f'CommunicationChannelID="{self.communication_channel_id}" '
+            f'communication_channel_id="{self.communication_channel_id}" '
             f'status_code="{self.status_code}" />'
         )
diff --git a/xknx/knxip/header.py b/xknx/knxip/header.py
index b657c9c0..211cbe0c 100644
--- a/xknx/knxip/header.py
+++ b/xknx/knxip/header.py
@@ -55,7 +55,7 @@ class KNXIPHeader:
             + self.total_length.to_bytes(2, "big")
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<KNXIPHeader "
diff --git a/xknx/knxip/routing_indication.py b/xknx/knxip/routing_indication.py
index 3c3978b8..e60fe7eb 100644
--- a/xknx/knxip/routing_indication.py
+++ b/xknx/knxip/routing_indication.py
@@ -49,6 +49,6 @@ class RoutingIndication(KNXIPBody):
             raise CouldNotParseKNXIP("No CEMIFrame defined.")
         return self.cemi.to_knx()
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return f'<RoutingIndication cemi="{self.cemi}" />'
diff --git a/xknx/knxip/search_request.py b/xknx/knxip/search_request.py
index 96d31ec2..ec5f0444 100644
--- a/xknx/knxip/search_request.py
+++ b/xknx/knxip/search_request.py
@@ -31,6 +31,6 @@ class SearchRequest(KNXIPBody):
         """Serialize to KNX/IP raw data."""
         return self.discovery_endpoint.to_knx()
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return f'<SearchRequest discovery_endpoint="{self.discovery_endpoint}" />'
diff --git a/xknx/knxip/search_request_extended.py b/xknx/knxip/search_request_extended.py
index c31095f0..c5e6d88e 100644
--- a/xknx/knxip/search_request_extended.py
+++ b/xknx/knxip/search_request_extended.py
@@ -47,8 +47,10 @@ class SearchRequestExtended(KNXIPBody):
             bytes(srp) for srp in self.srps
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
-            f'<SearchRequestExtended discovery_endpoint="{self.discovery_endpoint}" />'
+            "<SearchRequestExtended "
+            f'discovery_endpoint="{self.discovery_endpoint}" '
+            f'srps="{self.srps}" />'
         )
diff --git a/xknx/knxip/search_response.py b/xknx/knxip/search_response.py
index 4fd12cd1..ba29f902 100644
--- a/xknx/knxip/search_response.py
+++ b/xknx/knxip/search_response.py
@@ -51,7 +51,7 @@ class SearchResponse(KNXIPBody):
             dib.to_knx() for dib in self.dibs
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         _dibs_str = ",\n".join(dib.__str__() for dib in self.dibs)
         return (
diff --git a/xknx/knxip/search_response_extended.py b/xknx/knxip/search_response_extended.py
index 8def453f..d9dc90cd 100644
--- a/xknx/knxip/search_response_extended.py
+++ b/xknx/knxip/search_response_extended.py
@@ -25,7 +25,7 @@ class SearchResponseExtended(SearchResponse):
         """Initialize SearchResponse object."""
         super().__init__(control_endpoint)
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         _dibs_str = ",\n".join(dib.__str__() for dib in self.dibs)
         return (
diff --git a/xknx/knxip/secure_wrapper.py b/xknx/knxip/secure_wrapper.py
index e2c570f3..4545842a 100644
--- a/xknx/knxip/secure_wrapper.py
+++ b/xknx/knxip/secure_wrapper.py
@@ -85,7 +85,7 @@ class SecureWrapper(KNXIPBody):
             + self.message_authentication_code
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             f"<SecureWrapper "
diff --git a/xknx/knxip/session_authenticate.py b/xknx/knxip/session_authenticate.py
index a3dd70a2..0e33ca22 100644
--- a/xknx/knxip/session_authenticate.py
+++ b/xknx/knxip/session_authenticate.py
@@ -60,7 +60,7 @@ class SessionAuthenticate(KNXIPBody):
             + self.message_authentication_code
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             f"<SessionAuthenticate "
diff --git a/xknx/knxip/session_request.py b/xknx/knxip/session_request.py
index e84cd704..dd13db1b 100644
--- a/xknx/knxip/session_request.py
+++ b/xknx/knxip/session_request.py
@@ -47,7 +47,7 @@ class SessionRequest(KNXIPBody):
         """Serialize to KNX/IP raw data."""
         return self.control_endpoint.to_knx() + self.ecdh_client_public_key
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             f"<SessionRequest "
diff --git a/xknx/knxip/session_response.py b/xknx/knxip/session_response.py
index 39d02149..b2fba50f 100644
--- a/xknx/knxip/session_response.py
+++ b/xknx/knxip/session_response.py
@@ -57,7 +57,7 @@ class SessionResponse(KNXIPBody):
             + self.message_authentication_code
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             f"<SessionResponse "
diff --git a/xknx/knxip/session_status.py b/xknx/knxip/session_status.py
index 3dc56448..f95ecbc0 100644
--- a/xknx/knxip/session_status.py
+++ b/xknx/knxip/session_status.py
@@ -53,6 +53,6 @@ class SessionStatus(KNXIPBody):
             )
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return f'<SessionStatus status="{self.status}" />'
diff --git a/xknx/knxip/srp.py b/xknx/knxip/srp.py
index ebc3fa48..8ea4c3c3 100644
--- a/xknx/knxip/srp.py
+++ b/xknx/knxip/srp.py
@@ -145,7 +145,7 @@ class SRP:
         """
         return SRP(
             srp_type=SearchRequestParameterType.REQUEST_DIBS,
-            mandatory=True,
+            mandatory=False,
             data=bytes(dib.value for dib in dibs),
         )
 
diff --git a/xknx/knxip/tunnelling_ack.py b/xknx/knxip/tunnelling_ack.py
index f31432e3..844cb6b3 100644
--- a/xknx/knxip/tunnelling_ack.py
+++ b/xknx/knxip/tunnelling_ack.py
@@ -51,7 +51,7 @@ class TunnellingAck(KNXIPBodyResponse):
             )
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<TunnellingAck "
diff --git a/xknx/knxip/tunnelling_request.py b/xknx/knxip/tunnelling_request.py
index 0742dedf..d8c11750 100644
--- a/xknx/knxip/tunnelling_request.py
+++ b/xknx/knxip/tunnelling_request.py
@@ -80,7 +80,7 @@ class TunnellingRequest(KNXIPBody):
             + self.cemi.to_knx()
         )
 
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """Return object as readable string."""
         return (
             "<TunnellingRequest "
diff --git a/xknx/xknx.py b/xknx/xknx.py
index 0c0caa09..0f07fca7 100644
--- a/xknx/xknx.py
+++ b/xknx/xknx.py
@@ -115,7 +115,7 @@ class XKNX:
         """Start XKNX module. Connect to KNX/IP devices and start state updater."""
         if self.connection_config.threaded:
             await self.connection_manager.register_loop()
-        self.task_registry.start()
+            await self.task_registry.register_loop()
         self.knxip_interface = knx_interface_factory(
             xknx=self,
             connection_config=self.connection_config,
@@ -128,6 +128,7 @@ class XKNX:
         await self.knxip_interface.start()
         await self.telegram_queue.start()
         self.state_updater.start()
+        self.task_registry.start()
         self.started.set()
         if self.daemon_mode:
             await self.loop_until_sigint()


Test patch:
diff --git a/test/knxip_tests/srp_test.py b/test/knxip_tests/srp_test.py
index 7855fe18..bd497817 100644
--- a/test/knxip_tests/srp_test.py
+++ b/test/knxip_tests/srp_test.py
@@ -71,11 +71,11 @@ class TestKNXIPSRP:
                 SRP.request_device_description(
                     [DIBTypeCode.SUPP_SVC_FAMILIES, DIBTypeCode.TUNNELING_INFO]
                 ),
-                bytes.fromhex("04 84 02 07"),
+                bytes.fromhex("04 04 02 07"),
             ),
             (
                 SRP.request_device_description([DIBTypeCode.SUPP_SVC_FAMILIES]),
-                bytes.fromhex("04 84 02 00"),
+                bytes.fromhex("04 04 02 00"),
             ),
         ],
     )
diff --git a/test/str_test.py b/test/str_test.py
index 5d50d9fc..8bca0a84 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" />'
+            'request_type="ConnectRequestType.TUNNEL_CONNECTION" flags="0x2" />'
         )
 
     def test_connect_response(self):
@@ -550,7 +550,7 @@ class TestStringRepresentations:
         disconnect_request.control_endpoint = HPAI(ip_addr="192.168.42.1", port=33941)
         assert (
             str(disconnect_request)
-            == '<DisconnectRequest CommunicationChannelID="13" control_endpoint="192.168.42.1:33941/udp" />'
+            == '<DisconnectRequest communication_channel_id="13" control_endpoint="192.168.42.1:33941/udp" />'
         )
 
     def test_disconnect_response(self):
@@ -559,7 +559,7 @@ class TestStringRepresentations:
         disconnect_response.communication_channel_id = 23
         assert (
             str(disconnect_response)
-            == '<DisconnectResponse CommunicationChannelID="23" status_code="ErrorCode.E_NO_ERROR" />'
+            == '<DisconnectResponse communication_channel_id="23" status_code="ErrorCode.E_NO_ERROR" />'
         )
 
     def test_connectionstate_request(self):
@@ -571,7 +571,7 @@ class TestStringRepresentations:
         )
         assert (
             str(connectionstate_request)
-            == '<ConnectionStateRequest CommunicationChannelID="23", control_endpoint="192.168.42.1:33941/udp" />'
+            == '<ConnectionStateRequest communication_channel_id="23", control_endpoint="192.168.42.1:33941/udp" />'
         )
 
     def test_connectionstate_response(self):
@@ -580,7 +580,7 @@ class TestStringRepresentations:
         connectionstate_response.communication_channel_id = 23
         assert (
             str(connectionstate_response)
-            == '<ConnectionStateResponse CommunicationChannelID="23" status_code="ErrorCode.E_NO_ERROR" />'
+            == '<ConnectionStateResponse communication_channel_id="23" status_code="ErrorCode.E_NO_ERROR" />'
         )
 
     def test_search_reqeust(self):