Skip to content

Commit 46e9cbe

Browse files
authored
Add data_secure flag to Telegram (#1799)
* Add data_secure flag to Telegram * Update telegram.py * Update telegram.py
1 parent 4f33103 commit 46e9cbe

File tree

7 files changed

+71
-41
lines changed

7 files changed

+71
-41
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ nav_order: 2
88

99
# Unreleased
1010

11+
### Telegram
12+
13+
- Add `data_secure` flag to Telegram to indicate if it was sent or received as Data Secure.
14+
1115
### Devices
1216

1317
- ExposeSensor: `cooldown` is extended to wait for connection if not established.

test/cemi_tests/cemi_handler_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async def test_wait_for_l2_confirmation(time_travel: EventLoopClockAdvancer) ->
4545
await task
4646
assert xknx.connection_manager.cemi_count_outgoing == 1
4747
assert xknx.connection_manager.cemi_count_outgoing_error == 0
48+
assert test_telegram.data_secure is False
4849

4950
# no L_DATA.con received -> raise ConfirmationError
5051
xknx.knxip_interface.send_cemi.reset_mock()

test/remote_value_tests/remote_value_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ def test_pre_decoded_telegram(self) -> None:
275275
telegram = Telegram(
276276
destination_address=GroupAddress("1/1/1"),
277277
payload=GroupValueWrite(test_payload),
278-
decoded_data=TelegramDecodedData(transcoder=DPT2ByteFloat, value=3.3),
279278
)
279+
telegram.decoded_data = TelegramDecodedData(transcoder=DPT2ByteFloat, value=3.3)
280280
assert remote_value.process(telegram)
281281
assert remote_value.value == 3.3
282282

test/telegram_tests/telegram_test.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
"""Unit test for Telegram objects."""
22

3-
from xknx.dpt import DPTBinary
4-
from xknx.telegram import GroupAddress, IndividualAddress, Telegram, TelegramDirection
3+
from xknx.dpt import DPTBinary, DPTSwitch
4+
from xknx.telegram import (
5+
GroupAddress,
6+
IndividualAddress,
7+
Telegram,
8+
TelegramDecodedData,
9+
TelegramDirection,
10+
)
511
from xknx.telegram.apci import GroupValueRead, GroupValueWrite
612
from xknx.telegram.tpci import TConnect, TDisconnect
713

@@ -14,9 +20,14 @@ class TestTelegram:
1420
#
1521
def test_telegram_equal(self) -> None:
1622
"""Test equals operator."""
17-
assert Telegram(GroupAddress("1/2/3"), payload=GroupValueRead()) == Telegram(
18-
GroupAddress("1/2/3"), payload=GroupValueRead()
19-
)
23+
test = Telegram(GroupAddress("1/2/3"), payload=GroupValueRead())
24+
telegram_1 = Telegram(GroupAddress("1/2/3"), payload=GroupValueRead())
25+
assert test == telegram_1
26+
# decoded_data and data_secure should not be considered for equality (although
27+
# decoded_data doesn't make sense for a GroupValueRead, this is just for testing the equality operator)
28+
telegram_1.decoded_data = TelegramDecodedData(transcoder=DPTSwitch, value=False)
29+
telegram_1.data_secure = True
30+
assert test == telegram_1
2031

2132
def test_telegram_not_equal(self) -> None:
2233
"""Test not equals operator."""

xknx/cemi/cemi_handler.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ async def send_telegram(self, telegram: Telegram) -> None:
7272
logger.debug("Outgoing CEMI: %s", cemi)
7373
if self.data_secure is not None:
7474
cemi.data = self.data_secure.outgoing_cemi(cemi_data=cemi_data)
75+
telegram.data_secure = is_data_secure(cemi.data)
76+
else:
77+
telegram.data_secure = False
78+
7579
self._l_data_confirmation_event.clear()
7680
try:
7781
await self.xknx.knxip_interface.send_cemi(cemi)
@@ -123,13 +127,14 @@ def handle_cemi_frame(self, cemi: CEMIFrame) -> None:
123127
logger.debug("Incoming CEMI: %s", cemi)
124128
self.xknx.connection_manager.cemi_count_incoming += 1
125129

130+
_cemi_data_is_data_secure = is_data_secure(cemi.data)
126131
if self.data_secure is None:
127-
if is_data_secure(cemi.data):
132+
if _cemi_data_is_data_secure:
128133
data_secure_logger.debug(
129134
"Received DataSecure encrypted CEMI frame but no keys for DataSecure are initialized: %s",
130135
cemi,
131136
)
132-
self.handle_data_secure_key_issue(cemi.data)
137+
self.handle_data_secure_key_issue(cemi.data, _cemi_data_is_data_secure)
133138
return
134139
else:
135140
try:
@@ -140,11 +145,12 @@ def handle_cemi_frame(self, cemi: CEMIFrame) -> None:
140145
"Could not decrypt CEMI frame: %s",
141146
err,
142147
)
143-
self.handle_data_secure_key_issue(cemi.data)
148+
self.handle_data_secure_key_issue(cemi.data, _cemi_data_is_data_secure)
144149
return
145150

146151
telegram = cemi.data.telegram()
147152
telegram.direction = TelegramDirection.INCOMING
153+
telegram.data_secure = _cemi_data_is_data_secure
148154
self.telegram_received(telegram)
149155

150156
def telegram_received(self, telegram: Telegram) -> None:
@@ -159,10 +165,13 @@ def telegram_received(self, telegram: Telegram) -> None:
159165
return
160166
self.xknx.management.process(telegram)
161167

162-
def handle_data_secure_key_issue(self, cemi_data: CEMILData) -> None:
168+
def handle_data_secure_key_issue(
169+
self, cemi_data: CEMILData, received_data_secure: bool
170+
) -> None:
163171
"""Handle DataSecure telegrams with missing or invalid keys."""
164172
self.xknx.connection_manager.undecoded_data_secure += 1
165173
if isinstance(cemi_data.tpci, tpci.TDataGroup):
166174
telegram = cemi_data.telegram()
167175
telegram.direction = TelegramDirection.INCOMING
176+
telegram.data_secure = received_data_secure
168177
self.xknx.telegram_queue.received_data_secure_group_key_issue(telegram)

xknx/core/telegram_queue.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ def received_data_secure_group_key_issue(self, telegram: Telegram) -> None:
276276
"""
277277
Run registered callbacks for data secure group key issues.
278278
279-
Only TDataGroup telegrams with undecodable DataSecure payloads are forwarded.
279+
TDataGroup telegrams with undecodable DataSecure payloads
280+
or unexpected plain payloads are forwarded.
280281
"""
281282
for data_secure_group_key_issue_cb in self._data_secure_group_key_issue_cbs:
282283
try:

xknx/telegram/telegram.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,4 @@
1-
"""
2-
Module for KNX Telegrams.
3-
4-
The telegram class is the lightweight data transfer object between
5-
6-
* business logic (Lights, Covers, etc) and
7-
* underlying KNX/IP abstraction (CEMIHandler).
8-
9-
It contains
10-
11-
* the group address (e.g. GroupAddress("1/2/3"))
12-
* the direction (Incoming or Outgoing)
13-
* and the payload (e.g. GroupValueWrite(DPTBinary(False)))
14-
* the source address (e.g. IndividualAddress("1.2.3"))
15-
* the TPCI (Transport Layer Control Information) (e.g. TDataGroup())
16-
"""
1+
"""Module for KNX Telegrams."""
172

183
from __future__ import annotations
194

@@ -51,16 +36,46 @@ def __str__(self) -> str:
5136

5237
@dataclass(slots=True)
5338
class Telegram:
54-
"""Class for KNX telegrams."""
39+
"""
40+
Data transfer object for KNX telegrams.
41+
42+
Represents a message exchanged on the KNX bus between the business logic
43+
(Devices, Management, etc.) and the underlying KNX/IP abstraction layer.
44+
45+
Attributes:
46+
destination_address: Target GroupAddress, IndividualAddress, or
47+
InternalGroupAddress.
48+
direction: Communication direction (INCOMING or OUTGOING).
49+
payload: APCi payload containing the actual data (e.g., GroupValueWrite,
50+
GroupValueResponse). None for control information only telegrams.
51+
source_address: IndividualAddress of the sender. When default of 0.0.0 is
52+
used, it will be set automatically when sent.
53+
tpci: Transport Layer Control Information (TDataBroadcast, TDataGroup, or
54+
TDataIndividual). If not provided, it will be automatically inferred
55+
based on destination_address type.
56+
decoded_data: Optional decoded version of the payload including the
57+
transcoder class and decoded value. Set externally by GroupAddressDPT
58+
for convenience when the payload has already been decoded.
59+
data_secure: Flag indicating if the telegram was sent or received as
60+
DataSecure. Set externally by CEMIHandler. None if not yet processed.
61+
62+
"""
5563

5664
destination_address: GroupAddress | IndividualAddress | InternalGroupAddress
5765
direction: TelegramDirection = TelegramDirection.OUTGOING
5866
payload: APCI | None = None
5967
source_address: IndividualAddress = field(
6068
default_factory=lambda: IndividualAddress(0)
6169
)
62-
tpci: TPCI = None # type: ignore[assignment] # set in __post_init__
63-
decoded_data: TelegramDecodedData | None = None
70+
tpci: TPCI = None # type: ignore[assignment] # set by initializer or in __post_init__
71+
# set by GroupAddressDPT
72+
decoded_data: TelegramDecodedData | None = field(
73+
init=False, default=None, compare=False, hash=False
74+
)
75+
# flag if telegram was sent or received as DataSecure, set by CEMIHandler
76+
data_secure: bool | None = field(
77+
init=False, default=None, compare=False, hash=False
78+
)
6479

6580
def __post_init__(self) -> None:
6681
"""Initialize Telegram class."""
@@ -75,17 +90,6 @@ def __post_init__(self) -> None:
7590
else: # InternalGroupAddress
7691
self.tpci = TDataGroup()
7792

78-
def __eq__(self, other: object) -> bool:
79-
"""Equal operator. Omit decoded_data for comparison."""
80-
return (
81-
isinstance(other, Telegram)
82-
and self.destination_address == other.destination_address
83-
and self.direction == other.direction
84-
and self.payload == other.payload
85-
and self.source_address == other.source_address
86-
and self.tpci == other.tpci
87-
)
88-
8993
def __str__(self) -> str:
9094
"""Return object as readable string."""
9195
data = f'payload="{self.payload}"' if self.payload else f'tpci="{self.tpci}"'

0 commit comments

Comments
 (0)