Repository: XKNX/xknx
Pull request #1127 (2023-01-03 15:16:22)

Parse T_Data_Broadcast TPCI
<!--
  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.
-->
- Parse T_Data_Broadcast TPCI. 
Handle these telegrams in the Management class (currently only log a warning)

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)
- [x] 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:

- [ ] 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://codecov.io/gh/XKNX/xknx/pull/1127?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1127](https://codecov.io/gh/XKNX/xknx/pull/1127?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (a4471f5) into [main](https://codecov.io/gh/XKNX/xknx/commit/0f15ad4c5f4331ec3fab691a99c65ccd234d60a7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (0f15ad4) will **increase** coverage by `0.00%`.
> The diff coverage is `94.44%`.

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

```diff
@@           Coverage Diff           @@
##             main    #1127   +/-   ##
=======================================
  Coverage   96.34%   96.35%           
=======================================
  Files         138      138           
  Lines        8644     8661   +17     
=======================================
+ Hits         8328     8345   +17     
  Misses        316      316           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1127?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/io/knxip\_interface.py](https://codecov.io/gh/XKNX/xknx/pull/1127/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9rbnhpcF9pbnRlcmZhY2UucHk=) | `84.28% <60.00%> (+0.15%)` | :arrow_up: |
| [xknx/knxip/cemi\_frame.py](https://codecov.io/gh/XKNX/xknx/pull/1127/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9jZW1pX2ZyYW1lLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/telegram/address.py](https://codecov.io/gh/XKNX/xknx/pull/1127/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90ZWxlZ3JhbS9hZGRyZXNzLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/telegram/telegram.py](https://codecov.io/gh/XKNX/xknx/pull/1127/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90ZWxlZ3JhbS90ZWxlZ3JhbS5weQ==) | `95.34% <100.00%> (+1.23%)` | :arrow_up: |
| [xknx/telegram/tpci.py](https://codecov.io/gh/XKNX/xknx/pull/1127/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC90ZWxlZ3JhbS90cGNpLnB5) | `98.91% <100.00%> (+0.08%)` | :arrow_up: |


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 6006fcce..c86b736c 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,12 @@ nav_order: 2
 
 # Changelog
 
+# Unreleased changes
+
+### Internals
+
+- Parse T_Data_Broadcast TPCI. Forward these telegrams to the Management class.
+
 # 2.2.0 Expose cooldown 2022-12-27
 
 ### Devices
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index 793101bc..5e9d67e1 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -22,6 +22,7 @@ from xknx.exceptions import (
 from xknx.io import util
 from xknx.secure.keyring import XMLInterface, load_keyring
 from xknx.telegram import IndividualAddress, Telegram
+from xknx.telegram.tpci import TDataGroup
 
 from .connection import ConnectionConfig, ConnectionType
 from .const import DEFAULT_INDIVIDUAL_ADDRESS, DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
@@ -432,13 +433,13 @@ class KNXIPInterface:
 
     async def telegram_received(self, telegram: Telegram) -> list[Telegram] | None:
         """Put received telegram into queue. Callback for having received telegram."""
+        if isinstance(telegram.tpci, TDataGroup):
+            self.xknx.telegrams.put_nowait(telegram)
+            return None
         if isinstance(telegram.destination_address, IndividualAddress):
             if telegram.destination_address != self.xknx.current_address:
                 return None
-            return self.xknx.management.process(telegram)
-
-        self.xknx.telegrams.put_nowait(telegram)
-        return None
+        return self.xknx.management.process(telegram)
 
     async def send_telegram(self, telegram: Telegram) -> None:
         """Send telegram to connected device (either Tunneling or Routing)."""
diff --git a/xknx/knxip/cemi_frame.py b/xknx/knxip/cemi_frame.py
index 2af249bf..06a82e37 100644
--- a/xknx/knxip/cemi_frame.py
+++ b/xknx/knxip/cemi_frame.py
@@ -16,7 +16,7 @@ from __future__ import annotations
 from xknx.exceptions import ConversionError, UnsupportedCEMIMessage
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import APCI
-from xknx.telegram.tpci import TPCI, TDataGroup
+from xknx.telegram.tpci import TPCI, TDataBroadcast, TDataGroup
 
 from .knxip_enum import CEMIFlags, CEMIMessageCode
 
@@ -82,7 +82,11 @@ class CEMIFrame:
         )
 
         if isinstance(telegram.destination_address, GroupAddress):
-            self.flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS | CEMIFlags.PRIORITY_LOW
+            self.flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS
+            if isinstance(telegram.tpci, TDataBroadcast):
+                self.flags |= CEMIFlags.PRIORITY_SYSTEM
+            else:
+                self.flags |= CEMIFlags.PRIORITY_LOW
         elif isinstance(telegram.destination_address, IndividualAddress):
             self.flags |= (
                 CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS | CEMIFlags.PRIORITY_SYSTEM
@@ -143,10 +147,10 @@ class CEMIFrame:
         # Control field 1 and Control field 2 - first 2 octets after Additional information
         self.flags = cemi[2 + addil] * 256 + cemi[3 + addil]
 
-        self.src_addr = IndividualAddress((cemi[4 + addil], cemi[5 + addil]))
+        self.src_addr = IndividualAddress(cemi[4 + addil] * 256 + cemi[5 + addil])
 
         dst_is_group_address = bool(self.flags & CEMIFlags.DESTINATION_GROUP_ADDRESS)
-        dst_raw_address = (cemi[6 + addil], cemi[7 + addil])
+        dst_raw_address = cemi[6 + addil] * 256 + cemi[7 + addil]
         self.dst_addr = (
             GroupAddress(dst_raw_address)
             if dst_is_group_address
@@ -169,7 +173,9 @@ class CEMIFrame:
         # APCI (application layer control information) -> Last  10 bit of TPCI/APCI
         try:
             self.tpci = TPCI.resolve(
-                raw_tpci=tpdu[0], dst_is_group_address=dst_is_group_address
+                raw_tpci=tpdu[0],
+                dst_is_group_address=dst_is_group_address,
+                dst_is_zero=not dst_raw_address,
             )
         except ConversionError as err:
             raise UnsupportedCEMIMessage(f"TPCI not supported: {tpdu[0]:#10b}") from err
diff --git a/xknx/telegram/address.py b/xknx/telegram/address.py
index 604495bd..f218a3c5 100644
--- a/xknx/telegram/address.py
+++ b/xknx/telegram/address.py
@@ -84,13 +84,11 @@ class BaseAddress(ABC):
         Returns `True` if we check against the same subclass and the
         raw Value matches.
         """
-        if isinstance(self, type(other)):
-            return self.__hash__() == other.__hash__()
-        return False
+        return self.__hash__() == other.__hash__()
 
     def __hash__(self) -> int:
         """Hash Address so it can be used as dict key."""
-        return self.raw
+        return hash((self.__class__, self.raw))
 
 
 class IndividualAddress(BaseAddress):
@@ -363,10 +361,8 @@ class InternalGroupAddress:
         Returns `True` if we check against the same subclass and the
         raw Value matches.
         """
-        if isinstance(self, type(other)):
-            return self.__hash__() == other.__hash__()
-        return False
+        return self.__hash__() == other.__hash__()
 
     def __hash__(self) -> int:
         """Hash Address so it can be used as dict key."""
-        return hash(self.address)
+        return hash((self.__class__, self.address))
diff --git a/xknx/telegram/telegram.py b/xknx/telegram/telegram.py
index b37d77bd..64770c1b 100644
--- a/xknx/telegram/telegram.py
+++ b/xknx/telegram/telegram.py
@@ -20,7 +20,7 @@ from enum import Enum
 
 from .address import GroupAddress, IndividualAddress, InternalGroupAddress
 from .apci import APCI
-from .tpci import TPCI, TDataGroup, TDataIndividual
+from .tpci import TPCI, TDataBroadcast, TDataGroup, TDataIndividual
 
 
 class TelegramDirection(Enum):
@@ -50,11 +50,19 @@ class Telegram:
         self.payload = payload
         self.source_address = source_address or IndividualAddress(0)
         self.timestamp = datetime.now()
-        self.tpci = tpci or (
-            TDataIndividual()
-            if isinstance(destination_address, IndividualAddress)
-            else TDataGroup()
-        )
+        self.tpci: TPCI
+        if tpci is None:
+            if isinstance(destination_address, GroupAddress):
+                if destination_address.raw == 0:
+                    self.tpci = TDataBroadcast()
+                else:
+                    self.tpci = TDataGroup()
+            elif isinstance(destination_address, IndividualAddress):
+                self.tpci = TDataIndividual()
+            else:  # InternalGroupAddress
+                self.tpci = TDataGroup()
+        else:
+            self.tpci = tpci
 
     def __str__(self) -> str:
         """Return object as readable string."""
diff --git a/xknx/telegram/tpci.py b/xknx/telegram/tpci.py
index 8d7b24a5..c904c085 100644
--- a/xknx/telegram/tpci.py
+++ b/xknx/telegram/tpci.py
@@ -52,7 +52,7 @@ class TPCI(ABC):
         return f"{self.__class__.__name__}({_sequence_number})"
 
     @staticmethod
-    def resolve(raw_tpci: int, dst_is_group_address: bool) -> TPCI:
+    def resolve(raw_tpci: int, dst_is_group_address: bool, dst_is_zero: bool) -> TPCI:
         """
         Return TPCI instance from TPCI command.
 
@@ -66,6 +66,8 @@ class TPCI(ABC):
             if control or numbered:
                 raise ConversionError("Invalid TPCI flags in group addressed frame.")
             if not sequence_number:
+                if dst_is_zero:
+                    return TDataBroadcast()
                 return TDataGroup()
             if sequence_number == 1:
                 # TDataTagGroup uses sequence number field as flag
@@ -96,11 +98,18 @@ class TPCI(ABC):
 
 
 class TDataGroup(TPCI):
-    """
-    T_Data_Group class.
+    """T_Data_Group class."""
 
-    Includes T_Data_Broadcast and T_Data_Group depending on destination address.
-    """
+    control = False
+    numbered = False
+
+    def to_knx(self) -> int:
+        """Serialize to KNX/IP raw data."""
+        return 0
+
+
+class TDataBroadcast(TPCI):
+    """T_Data_Broadcast class."""
 
     control = False
     numbered = False


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index bad9d5c4..078978df 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -3,11 +3,11 @@ pre-commit==2.21.0
 isort==5.11.4
 flake8==6.0.0
 flake8-isort==6.0.0
-pydocstyle==6.2.1
+pydocstyle==6.2.2
 pylint==2.15.9
 pytest==7.2.0
 pytest-asyncio==0.20.3
 pytest-cov==4.0.0
 setuptools==65.6.3
-tox==4.1.3
+tox==4.2.1
 mypy==0.991
diff --git a/test/core_tests/telegram_queue_test.py b/test/core_tests/telegram_queue_test.py
index 10389e72..523ffc7b 100644
--- a/test/core_tests/telegram_queue_test.py
+++ b/test/core_tests/telegram_queue_test.py
@@ -55,11 +55,13 @@ class TestTelegramQueue:
         sleep_time = 0.05  # 1 / 20
 
         telegram_in = Telegram(
+            destination_address=GroupAddress("1/2/3"),
             direction=TelegramDirection.INCOMING,
             payload=GroupValueWrite(DPTBinary(1)),
         )
 
         telegram_out = Telegram(
+            destination_address=GroupAddress("1/2/3"),
             direction=TelegramDirection.OUTGOING,
             payload=GroupValueWrite(DPTBinary(1)),
         )
diff --git a/test/devices_tests/device_test.py b/test/devices_tests/device_test.py
index 1e23909f..a72f6d17 100644
--- a/test/devices_tests/device_test.py
+++ b/test/devices_tests/device_test.py
@@ -132,7 +132,7 @@ class TestDevice:
         """Test if process_group_write. Nothing really to test here."""
         xknx = XKNX()
         device = Device(xknx, "TestDevice")
-        await device.process_group_write(Telegram())
+        await device.process_group_write(Telegram(destination_address=GroupAddress(1)))
 
     async def test_process_group_response(self):
         """Test if process_group_read. Testing if mapped to group_write."""
@@ -141,11 +141,15 @@ class TestDevice:
         with patch(
             "xknx.devices.Device.process_group_write", new_callable=AsyncMock
         ) as mock_group_write:
-            await device.process_group_response(Telegram())
-            mock_group_write.assert_called_with(Telegram())
+            await device.process_group_response(
+                Telegram(destination_address=GroupAddress(1))
+            )
+            mock_group_write.assert_called_with(
+                Telegram(destination_address=GroupAddress(1))
+            )
 
     async def test_process_group_read(self):
         """Test if process_group_read. Nothing really to test here."""
         xknx = XKNX()
         device = Device(xknx, "TestDevice")
-        await device.process_group_read(Telegram())
+        await device.process_group_read(Telegram(destination_address=GroupAddress(1)))
diff --git a/test/io_tests/routing_test.py b/test/io_tests/routing_test.py
index e2154816..e4628f48 100644
--- a/test/io_tests/routing_test.py
+++ b/test/io_tests/routing_test.py
@@ -42,7 +42,10 @@ class TestRouting:
         test_telegram = _cemi.telegram
         test_telegram.direction = TelegramDirection.INCOMING
 
-        response_telegram = Telegram(tpci=tpci.TDisconnect())
+        response_telegram = Telegram(
+            destination_address=IndividualAddress(test_telegram.source_address),
+            tpci=tpci.TDisconnect(),
+        )
         response_cemi = CEMIFrame.init_from_telegram(
             telegram=response_telegram,
             src_addr=DEFAULT_INDIVIDUAL_ADDRESS,
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
index 53178978..deb59f78 100644
--- a/test/io_tests/tunnel_test.py
+++ b/test/io_tests/tunnel_test.py
@@ -28,7 +28,7 @@ from xknx.knxip import (
     TunnellingRequest,
 )
 from xknx.knxip.knxip_enum import CEMIMessageCode, HostProtocol
-from xknx.telegram import IndividualAddress, Telegram, TelegramDirection
+from xknx.telegram import GroupAddress, IndividualAddress, Telegram, TelegramDirection
 from xknx.telegram.apci import GroupValueWrite
 
 
@@ -95,7 +95,10 @@ class TestUDPTunnel:
         test_telegram.direction = TelegramDirection.INCOMING
         self.tunnel.expected_sequence_number = 0x81
 
-        response_telegram = Telegram(source_address=self.tunnel._src_address)
+        response_telegram = Telegram(
+            destination_address=test_telegram.source_address,
+            source_address=self.tunnel._src_address,
+        )
 
         async def tg_received_mock(telegram):
             """Mock for telegram_received_callback."""
@@ -125,7 +128,10 @@ class TestUDPTunnel:
         self.tunnel.communication_channel = 1
         self.tunnel.expected_sequence_number = 10
 
-        test_telegram = Telegram(payload=GroupValueWrite(DPTArray((1,))))
+        test_telegram = Telegram(
+            destination_address=GroupAddress(1),
+            payload=GroupValueWrite(DPTArray((1,))),
+        )
         cemi = CEMIFrame.init_from_telegram(
             test_telegram, code=CEMIMessageCode.L_DATA_IND
         )
@@ -195,7 +201,10 @@ class TestUDPTunnel:
         self.tunnel.communication_channel = 1
         self.tunnel.expected_sequence_number = 23
 
-        test_telegram = Telegram(payload=GroupValueWrite(DPTArray((1,))))
+        test_telegram = Telegram(
+            destination_address=GroupAddress(1),
+            payload=GroupValueWrite(DPTArray((1,))),
+        )
         cemi = CEMIFrame.init_from_telegram(
             test_telegram, code=CEMIMessageCode.L_DATA_CON
         )
@@ -227,7 +236,10 @@ class TestUDPTunnel:
         self.tunnel.sequence_number = 23
         self.tunnel.expected_sequence_number = 15
 
-        test_telegram = Telegram(payload=GroupValueWrite(DPTArray((1,))))
+        test_telegram = Telegram(
+            destination_address=GroupAddress(1),
+            payload=GroupValueWrite(DPTArray((1,))),
+        )
         cemi = CEMIFrame.init_from_telegram(
             test_telegram,
             code=CEMIMessageCode.L_DATA_REQ,
@@ -333,7 +345,10 @@ class TestUDPTunnel:
 
         # Send - use data endpoint
         self.tunnel.transport.send.reset_mock()
-        test_telegram = Telegram(payload=GroupValueWrite(DPTArray((1,))))
+        test_telegram = Telegram(
+            destination_address=GroupAddress(1),
+            payload=GroupValueWrite(DPTArray((1,))),
+        )
         cemi = CEMIFrame.init_from_telegram(
             test_telegram,
             code=CEMIMessageCode.L_DATA_REQ,
@@ -417,7 +432,10 @@ class TestTCPTunnel:
         self.tunnel.transport.send = Mock()
         self.tunnel.communication_channel = 1
 
-        test_telegram = Telegram(payload=GroupValueWrite(DPTArray((1,))))
+        test_telegram = Telegram(
+            destination_address=GroupAddress(1),
+            payload=GroupValueWrite(DPTArray((1,))),
+        )
         cemi = CEMIFrame.init_from_telegram(
             test_telegram, code=CEMIMessageCode.L_DATA_CON
         )
diff --git a/test/knxip_tests/cemi_frame_test.py b/test/knxip_tests/cemi_frame_test.py
index 171dcacf..a1ace448 100644
--- a/test/knxip_tests/cemi_frame_test.py
+++ b/test/knxip_tests/cemi_frame_test.py
@@ -6,7 +6,7 @@ from xknx.knxip.cemi_frame import CEMIFrame
 from xknx.knxip.knxip_enum import CEMIFlags, CEMIMessageCode
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import GroupValueRead
-from xknx.telegram.tpci import TAck, TConnect, TDataGroup
+from xknx.telegram.tpci import TAck, TConnect, TDataBroadcast, TDataGroup
 
 
 def get_data(code, adil, flags, src, dst, npdu_len, tpci_apci, payload):
@@ -31,13 +31,13 @@ def get_data(code, adil, flags, src, dst, npdu_len, tpci_apci, payload):
 
 def test_valid_command():
     """Test for valid frame parsing."""
-    raw = get_data(0x29, 0, 0x0080, 0, 0, 1, 0, [])
+    raw = get_data(0x29, 0, 0x0080, 1, 1, 1, 0, [])
     frame = CEMIFrame()
     packet_len = frame.from_knx(raw)
     assert frame.code == CEMIMessageCode.L_DATA_IND
     assert frame.flags == 0x0080
-    assert frame.src_addr == IndividualAddress(0)
-    assert frame.dst_addr == GroupAddress(0)
+    assert frame.src_addr == IndividualAddress(1)
+    assert frame.dst_addr == GroupAddress(1)
     assert frame.payload == GroupValueRead()
     assert frame.tpci == TDataGroup()
     assert packet_len == 11
@@ -180,7 +180,7 @@ def test_from_knx_individual_address():
 def test_telegram_group_address():
     """Test telegram conversion flags with a group address."""
     frame = CEMIFrame()
-    _telegram = Telegram(destination_address=GroupAddress(0))
+    _telegram = Telegram(destination_address=GroupAddress(1))
     # test CEMIFrame.telegram setter
     frame.telegram = _telegram
     assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
@@ -189,6 +189,19 @@ def test_telegram_group_address():
     assert frame.telegram == _telegram
 
 
+def test_telegram_broadcast():
+    """Test telegram conversion flags with a group address."""
+    frame = CEMIFrame()
+    _telegram = Telegram(destination_address=GroupAddress(0))
+    # test CEMIFrame.telegram setter
+    frame.telegram = _telegram
+    assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
+    assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
+    assert frame.tpci == TDataBroadcast()
+    # test CEMIFrame.telegram property
+    assert frame.telegram == _telegram
+
+
 def test_telegram_individual_address():
     """Test telegram conversion flags with a individual address."""
     frame = CEMIFrame()
diff --git a/test/telegram_tests/tpci_test.py b/test/telegram_tests/tpci_test.py
index 725f344b..7e600755 100644
--- a/test/telegram_tests/tpci_test.py
+++ b/test/telegram_tests/tpci_test.py
@@ -6,6 +6,7 @@ from xknx.telegram.tpci import (
     TPCI,
     TAck,
     TConnect,
+    TDataBroadcast,
     TDataConnected,
     TDataGroup,
     TDataIndividual,
@@ -16,27 +17,37 @@ from xknx.telegram.tpci import (
 
 
 @pytest.mark.parametrize(
-    "tpci_int,dst_is_group_address,tpci_expected",
+    "tpci_int,dst_is_group_address,dst_is_zero,tpci_expected",
     [
-        (0b00000000, True, TDataGroup()),
-        (0b00000100, True, TDataTagGroup()),
-        (0b00000000, False, TDataIndividual()),
-        (0b01011100, False, TDataConnected(sequence_number=0b0111)),
-        (0b10000000, False, TConnect()),
-        (0b10000001, False, TDisconnect()),
-        (0b11101010, False, TAck(sequence_number=10)),
-        (0b11010011, False, TNak(sequence_number=4)),
+        (0b00000000, True, False, TDataGroup()),
+        (0b00000000, True, True, TDataBroadcast()),
+        (0b00000100, True, False, TDataTagGroup()),
+        (0b00000000, False, False, TDataIndividual()),
+        (0b00000000, False, True, TDataIndividual()),
+        (0b01011100, False, False, TDataConnected(sequence_number=0b0111)),
+        (0b10000000, False, False, TConnect()),
+        (0b10000000, False, True, TConnect()),
+        (0b10000001, False, False, TDisconnect()),
+        (0b11101010, False, False, TAck(sequence_number=10)),
+        (0b11010011, False, False, TNak(sequence_number=4)),
     ],
 )
-def test_tpci_resolve_encode(tpci_int, dst_is_group_address, tpci_expected):
+def test_tpci_resolve_encode(
+    tpci_int, dst_is_group_address, dst_is_zero, tpci_expected
+):
     """Test resolving and encoding TPCI classes."""
     assert (
-        TPCI.resolve(raw_tpci=tpci_int, dst_is_group_address=dst_is_group_address)
+        TPCI.resolve(
+            raw_tpci=tpci_int,
+            dst_is_group_address=dst_is_group_address,
+            dst_is_zero=dst_is_zero,
+        )
         == tpci_expected
     )
     assert tpci_expected.to_knx() == tpci_int
 
 
+@pytest.mark.parametrize("dst_is_zero", [True, False])
 @pytest.mark.parametrize(
     "tpci_int,dst_is_group_address",
     [
@@ -49,10 +60,14 @@ def test_tpci_resolve_encode(tpci_int, dst_is_group_address, tpci_expected):
         (0b01000000, True),
     ],
 )
-def test_invalid_tpci(tpci_int, dst_is_group_address):
+def test_invalid_tpci(tpci_int, dst_is_group_address, dst_is_zero):
     """Test resolving TPCI classes."""
     with pytest.raises(ConversionError):
-        TPCI.resolve(raw_tpci=tpci_int, dst_is_group_address=dst_is_group_address)
+        TPCI.resolve(
+            raw_tpci=tpci_int,
+            dst_is_group_address=dst_is_group_address,
+            dst_is_zero=dst_is_zero,
+        )
 
 
 def test_equality():