Skip to content

Commit 194e953

Browse files
authored
Add individual address to gateway descriptor and set the source address of outgoing telegrams correctly (#788)
1 parent 0091f06 commit 194e953

9 files changed

+74
-21
lines changed

test/io_tests/gateway_scanner_test.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class TestGatewayScanner:
4747
local_ip="192.168.42.50",
4848
supports_tunnelling=True,
4949
supports_routing=True,
50+
individual_address=IndividualAddress("1.1.0"),
5051
)
5152
gateway_desc_neither = GatewayDescriptor(
5253
name="AC/S 1.1.1 Application Control",
@@ -152,10 +153,31 @@ def test_search_response_reception(self):
152153

153154
assert gateway_scanner.found_gateways == []
154155
gateway_scanner._response_rec_callback(
155-
test_search_response, udp_client_mock, interface="en1"
156+
test_search_response,
157+
udp_client_mock,
158+
interface="en1",
159+
netmask="255.255.255.0",
156160
)
157161
assert str(gateway_scanner.found_gateways[0]) == str(self.gateway_desc_both)
158162

163+
def test_search_response_wrong_network(self):
164+
"""Test function of gateway scanner when network iface doesnt match search response."""
165+
xknx = XKNX()
166+
gateway_scanner = GatewayScanner(xknx)
167+
test_search_response = fake_router_search_response(xknx)
168+
udp_client_mock = create_autospec(UDPClient)
169+
udp_client_mock.local_addr = ("192.168.100.50", 0)
170+
udp_client_mock.getsockname.return_value = ("192.168.100.50", 0)
171+
172+
assert gateway_scanner.found_gateways == []
173+
gateway_scanner._response_rec_callback(
174+
test_search_response,
175+
udp_client_mock,
176+
interface="en1",
177+
netmask="255.255.255.0",
178+
)
179+
assert gateway_scanner.found_gateways == []
180+
159181
@patch("xknx.io.gateway_scanner.netifaces", autospec=True)
160182
async def test_scan_timeout(self, netifaces_mock):
161183
"""Test gateway scanner timeout."""
@@ -192,8 +214,8 @@ async def async_none():
192214

193215
assert _search_interface_mock.call_count == 2
194216
expected_calls = [
195-
((gateway_scanner, "lo0", "127.0.0.1"),),
196-
((gateway_scanner, "en1", "10.1.1.2"),),
217+
((gateway_scanner, "lo0", "127.0.0.1", "255.0.0.0"),),
218+
((gateway_scanner, "en1", "10.1.1.2", "255.255.255.0"),),
197219
]
198220
assert _search_interface_mock.call_args_list == expected_calls
199221
assert test_scan == []

test/str_test.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -664,11 +664,9 @@ def test_gateway_descriptor(self):
664664
local_ip="192.168.2.50",
665665
supports_tunnelling=True,
666666
supports_routing=False,
667+
individual_address=IndividualAddress("1.1.1"),
667668
)
668-
assert (
669-
str(gateway_descriptor)
670-
== '<GatewayDescriptor name="KNX-Interface" addr="192.168.2.3:1234" local="192.168.2.50@en1" routing="False" tunnelling="True" />'
671-
)
669+
assert str(gateway_descriptor) == "1.1.1 - KNX-Interface @ 192.168.2.3:1234"
672670

673671
#
674672
# Routing Indication

xknx/core/payload_reader.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ async def send(
8181
async def send_telegram(self, payload: APCI) -> None:
8282
"""Send the telegram."""
8383
await self.xknx.telegrams.put(
84-
Telegram(destination_address=self.address, payload=payload)
84+
Telegram(
85+
destination_address=self.address,
86+
payload=payload,
87+
source_address=self.xknx.current_address,
88+
)
8589
)
8690

8791
async def telegram_received(

xknx/core/value_reader.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ async def read(self) -> Telegram | None:
7070
async def send_group_read(self) -> None:
7171
"""Send group read."""
7272
telegram = Telegram(
73-
destination_address=self.group_address, payload=GroupValueRead()
73+
destination_address=self.group_address,
74+
payload=GroupValueRead(),
75+
source_address=self.xknx.current_address,
7476
)
7577
await self.xknx.telegrams.put(telegram)
7678

xknx/io/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# flake8: noqa
1010
from .connection import ConnectionConfig, ConnectionType
1111
from .const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
12-
from .gateway_scanner import GatewayScanFilter, GatewayScanner
12+
from .gateway_scanner import GatewayDescriptor, GatewayScanFilter, GatewayScanner
1313
from .knxip_interface import KNXIPInterface
1414
from .routing import Routing
1515
from .tunnel import Tunnel

xknx/io/gateway_scanner.py

+35-11
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,22 @@
99

1010
import asyncio
1111
from functools import partial
12+
from ipaddress import IPv4Address, IPv4Network
1213
import logging
1314
from typing import TYPE_CHECKING
1415

1516
import netifaces
1617
from xknx.knxip import (
1718
HPAI,
19+
DIBDeviceInformation,
1820
DIBServiceFamily,
1921
DIBSuppSVCFamilies,
2022
KNXIPFrame,
2123
KNXIPServiceType,
2224
SearchRequest,
2325
SearchResponse,
2426
)
27+
from xknx.telegram import IndividualAddress
2528

2629
from .udp_client import UDPClient
2730

@@ -43,6 +46,7 @@ def __init__(
4346
local_ip: str,
4447
supports_tunnelling: bool = False,
4548
supports_routing: bool = False,
49+
individual_address: IndividualAddress | None = None,
4650
):
4751
"""Initialize GatewayDescriptor class."""
4852
self.name = name
@@ -52,16 +56,11 @@ def __init__(
5256
self.local_ip = local_ip
5357
self.supports_routing = supports_routing
5458
self.supports_tunnelling = supports_tunnelling
59+
self.individual_address = individual_address
5560

5661
def __str__(self) -> str:
5762
"""Return object as readable string."""
58-
return (
59-
f'<GatewayDescriptor name="{self.name}" '
60-
f'addr="{self.ip_addr}:{self.port}" '
61-
f'local="{self.local_ip}@{self.local_interface}" '
62-
f'routing="{self.supports_routing}" '
63-
f'tunnelling="{self.supports_tunnelling}" />'
64-
)
63+
return f"{self.individual_address} - {self.name} @ {self.ip_addr}:{self.port}"
6564

6665

6766
class GatewayScanFilter:
@@ -146,12 +145,15 @@ async def _send_search_requests(self) -> None:
146145
try:
147146
af_inet = netifaces.ifaddresses(interface)[netifaces.AF_INET]
148147
ip_addr = af_inet[0]["addr"]
149-
await self._search_interface(interface, ip_addr)
148+
netmask = af_inet[0]["netmask"]
149+
await self._search_interface(interface, ip_addr, netmask)
150150
except KeyError:
151151
logger.info("Could not connect to an KNX/IP device on %s", interface)
152152
continue
153153

154-
async def _search_interface(self, interface: str, ip_addr: str) -> None:
154+
async def _search_interface(
155+
self, interface: str, ip_addr: str, netmask: str
156+
) -> None:
155157
"""Send a search request on a specific interface."""
156158
logger.debug("Searching on %s / %s", interface, ip_addr)
157159

@@ -163,7 +165,7 @@ async def _search_interface(self, interface: str, ip_addr: str) -> None:
163165
)
164166

165167
udp_client.register_callback(
166-
partial(self._response_rec_callback, interface=interface),
168+
partial(self._response_rec_callback, interface=interface, netmask=netmask),
167169
[KNXIPServiceType.SEARCH_RESPONSE],
168170
)
169171
await udp_client.connect()
@@ -177,13 +179,25 @@ async def _search_interface(self, interface: str, ip_addr: str) -> None:
177179
udp_client.send(KNXIPFrame.init_from_body(search_request))
178180

179181
def _response_rec_callback(
180-
self, knx_ip_frame: KNXIPFrame, udp_client: UDPClient, interface: str = ""
182+
self,
183+
knx_ip_frame: KNXIPFrame,
184+
udp_client: UDPClient,
185+
interface: str = "",
186+
netmask: str = "",
181187
) -> None:
182188
"""Verify and handle knxipframe. Callback from internal udpclient."""
183189
if not isinstance(knx_ip_frame.body, SearchResponse):
184190
logger.warning("Could not understand knxipframe")
185191
return
186192

193+
address: IPv4Address = IPv4Address(knx_ip_frame.body.control_endpoint.ip_addr)
194+
network: IPv4Network = IPv4Network(
195+
f"{udp_client.local_addr[0]}/{netmask}", False
196+
)
197+
198+
if address not in network:
199+
return
200+
187201
gateway = GatewayDescriptor(
188202
name=knx_ip_frame.body.device_name,
189203
ip_addr=knx_ip_frame.body.control_endpoint.ip_addr,
@@ -202,6 +216,16 @@ def _response_rec_callback(
202216
except StopIteration:
203217
pass
204218

219+
try:
220+
device_infos = next(
221+
device_infos
222+
for device_infos in knx_ip_frame.body.dibs
223+
if isinstance(device_infos, DIBDeviceInformation)
224+
)
225+
gateway.individual_address = device_infos.individual_address
226+
except StopIteration:
227+
pass
228+
205229
self._add_found_gateway(gateway)
206230

207231
def _add_found_gateway(self, gateway: GatewayDescriptor) -> None:

xknx/io/tunnel.py

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ async def _connect_request(self) -> bool:
184184
self.communication_channel = connect.communication_channel
185185
# Use the individual address provided by the tunnelling server
186186
self._src_address = IndividualAddress(connect.identifier)
187+
self.xknx.current_address = self._src_address
187188
logger.debug(
188189
"Tunnel established communication_channel=%s, id=%s",
189190
connect.communication_channel,

xknx/remote_value/remote_value.py

+1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ async def _send(
213213
if response
214214
else GroupValueWrite(payload)
215215
),
216+
source_address=self.xknx.current_address,
216217
)
217218
await self.xknx.telegrams.put(telegram)
218219

xknx/xknx.py

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(
7272
self.connection_config = connection_config
7373
self.daemon_mode = daemon_mode
7474
self.version = VERSION
75+
self.current_address = IndividualAddress(0)
7576

7677
if log_directory is not None:
7778
self.setup_logging(log_directory)

0 commit comments

Comments
 (0)