Repository: XKNX/xknx (python)

Issue:
Enhance HA knx_event
There are some requests about updating the HA knx_event. 
Eg. https://github.com/home-assistant/core/issues/43256

Currently we just fire the `knx_event` for incoming telegrams, as stated in the documentation.

We could
- [ ] fire on outgoing telegrams too, not only on incoming
- [x] add a parameter "direction" for incoming/outgoing
- [x] add a parameter "source" for IA (eg. to identify which wall switch was used)
- [x] add a parameter "type" for write or response (maybe also read)

I'm still not very convinced about using the `knx_event` at all since we can't really decode the payload and dealing with raw payloads should not be a user-facing task in HA (besides maybe for DPT 1/2/3).

Pull request description:
Feat: Also run callbacks on outgoing telegrams, but filter them out b…

<!--
  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.
-->

Do trigger telegram callbacks also for outgoing telegrams for HA. For all other callbacks this is disabled by default and has to be explicitly enabled.

Fixes #497 

## 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)
- [ ] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [x] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [x] The Homeassistant plugin has been adjusted in case of new config options


Patch:
diff --git a/changelog.md b/changelog.md
index 8692b1d2..b8648063 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,10 @@
 
 ## Unreleased changes
 
+### HA integration
+
+- knx_event: fire also for outgoing telegrams
+
 ### Devices
 
 - BinarySensor: return `None` for `BinarySensor.counter` when context timeout is not used (and don't calculate it)
diff --git a/home-assistant-plugin/custom_components/xknx/__init__.py b/home-assistant-plugin/custom_components/xknx/__init__.py
index fab7ec68..f7f63d8d 100644
--- a/home-assistant-plugin/custom_components/xknx/__init__.py
+++ b/home-assistant-plugin/custom_components/xknx/__init__.py
@@ -368,6 +368,7 @@ class KNXModule:
             self.telegram_received_cb,
             address_filters=address_filters,
             group_addresses=[],
+            match_for_outgoing_telegrams=True,
         )
 
     async def service_event_register_modify(self, call):
diff --git a/xknx/core/telegram_queue.py b/xknx/core/telegram_queue.py
index c793ebc4..ea68f572 100644
--- a/xknx/core/telegram_queue.py
+++ b/xknx/core/telegram_queue.py
@@ -37,15 +37,22 @@ class TelegramQueue:
             callback: "AsyncTelegramCallback",
             address_filters: Optional[List[AddressFilter]] = None,
             group_addresses: Optional[List[GroupAddress]] = None,
+            match_for_outgoing_telegrams: bool = False,
         ):
             """Initialize Callback class."""
             self.callback = callback
             self._match_all = address_filters is None and group_addresses is None
+            self._match_outgoing = match_for_outgoing_telegrams
             self.address_filters = [] if address_filters is None else address_filters
             self.group_addresses = [] if group_addresses is None else group_addresses
 
         def is_within_filter(self, telegram: Telegram) -> bool:
             """Test if callback is filtering for group address."""
+            if (
+                not self._match_outgoing
+                and telegram.direction == TelegramDirection.OUTGOING
+            ):
+                return False
             if self._match_all:
                 return True
             if isinstance(telegram.destination_address, GroupAddress):
@@ -69,12 +76,14 @@ class TelegramQueue:
         telegram_received_cb: "AsyncTelegramCallback",
         address_filters: Optional[List[AddressFilter]] = None,
         group_addresses: Optional[List[GroupAddress]] = None,
+        match_for_outgoing: bool = False,
     ) -> Callback:
-        """Register callback for a telegram beeing received from KNX bus."""
+        """Register callback for a telegram being received from KNX bus."""
         callback = TelegramQueue.Callback(
             telegram_received_cb,
             address_filters=address_filters,
             group_addresses=group_addresses,
+            match_for_outgoing_telegrams=match_for_outgoing,
         )
         self.telegram_received_cbs.append(callback)
         return callback
@@ -165,6 +174,10 @@ class TelegramQueue:
             await self.xknx.knxip_interface.send_telegram(telegram)
             if isinstance(telegram.payload, GroupValueWrite):
                 await self.xknx.devices.process(telegram)
+
+            for telegram_received_cb in self.telegram_received_cbs:
+                if telegram_received_cb.is_within_filter(telegram):
+                    await telegram_received_cb.callback(telegram)
         else:
             raise CommunicationError("No KNXIP interface defined")
 


Test patch:
diff --git a/test/core_tests/telegram_queue_test.py b/test/core_tests/telegram_queue_test.py
index 9543f66b..6ef23d1b 100644
--- a/test/core_tests/telegram_queue_test.py
+++ b/test/core_tests/telegram_queue_test.py
@@ -117,10 +117,62 @@ class TestTelegramQueue(unittest.TestCase):
             payload=GroupValueWrite(DPTBinary(1)),
         )
         self.loop.run_until_complete(
-            xknx.telegram_queue.process_telegram_incoming(telegram)
+            xknx.telegram_queue.process_telegram_incoming(telegram),
         )
         async_telegram_received_cb.assert_called_once_with(telegram)
 
+    @patch("xknx.io.KNXIPInterface")
+    def test_register_with_outgoing_telegrams(self, if_mock):
+        """Test telegram_received_callback with outgoing telegrams."""
+        # pylint: disable=no-self-use
+        xknx = XKNX()
+        async_telegram_received_cb = AsyncMock()
+
+        async_if_send_telegram = asyncio.Future()
+        async_if_send_telegram.set_result(None)
+        if_mock.send_telegram.return_value = async_if_send_telegram
+
+        xknx.telegram_queue.register_telegram_received_cb(
+            async_telegram_received_cb, None, None, True
+        )
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            direction=TelegramDirection.OUTGOING,
+            payload=GroupValueWrite(DPTBinary(1)),
+        )
+
+        xknx.knxip_interface = if_mock
+        self.loop.run_until_complete(
+            xknx.telegram_queue.process_telegram_outgoing(telegram)
+        )
+        async_telegram_received_cb.assert_called_once_with(telegram)
+
+    @patch("xknx.io.KNXIPInterface")
+    def test_register_with_outgoing_telegrams_does_not_trigger(self, if_mock):
+        """Test telegram_received_callback with outgoing telegrams."""
+        # pylint: disable=no-self-use
+        xknx = XKNX()
+        async_telegram_received_cb = AsyncMock()
+
+        async_if_send_telegram = asyncio.Future()
+        async_if_send_telegram.set_result(None)
+        if_mock.send_telegram.return_value = async_if_send_telegram
+
+        xknx.telegram_queue.register_telegram_received_cb(async_telegram_received_cb)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            direction=TelegramDirection.OUTGOING,
+            payload=GroupValueWrite(DPTBinary(1)),
+        )
+
+        xknx.knxip_interface = if_mock
+        self.loop.run_until_complete(
+            xknx.telegram_queue.process_telegram_outgoing(telegram)
+        )
+        async_telegram_received_cb.assert_not_called()
+
     #
     # TEST UNREGISTER
     #