From e8df8f7b0c51999d94ebbf2c3e96b94c51b4b99f Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Wed, 29 Jul 2026 12:53:56 +0000 Subject: [PATCH] fix(config_flow): always retry historically-confirmed DTLS ports (#192) The liveness sweep's ICMP-based verdict isn't trustworthy on every network path -- a segregated-VLAN report showed it calling three closed ports live while missing the one port a concurrent nmap scan found genuinely open|filtered, which also happened to be one of our two historically confirmed DTLS ports. Rather than trust a "not live" verdict against that prior, always give PREFERRED_PROBE_PORTS a real handshake attempt even when the sweep excludes them, bounded to at most those two extra attempts. This is a stop-gap for the reported failure mode, not a full fix for the sweep's underlying unreliability -- left a comment on the issue with the diagnosis and flagged the sweep itself for a deeper redesign. --- custom_components/localthings/config_flow.py | 12 +++++++- tests/localthings/test_config_flow.py | 32 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/config_flow.py b/custom_components/localthings/config_flow.py index c2b50b1..f0ca90b 100644 --- a/custom_components/localthings/config_flow.py +++ b/custom_components/localthings/config_flow.py @@ -194,7 +194,17 @@ def _find_live_ports(host: str, ports: list[int], timeout: float) -> list[int]: except OSError: pass - return _order_candidates(live) + # The sweep's ICMP-based verdict isn't reliable on every network path -- + # issue #192 captured a segregated-VLAN device where it called three + # ports live that a concurrent nmap scan showed as closed, while the + # port nmap found genuinely open|filtered (49154, one of our historically + # confirmed ports) never showed up as live at all. Rather than trust a + # wrong "not live" verdict on a port we already have strong prior + # evidence for, always give the historically-confirmed ports a real + # handshake attempt too. Bounded cost: at most len(PREFERRED_PROBE_PORTS) + # extra handshakes, only when the sweep disagrees with the prior. + rescued = [p for p in PREFERRED_PROBE_PORTS if p in ports and p not in live] + return _order_candidates(live + rescued) def _is_placeholder_serial(serial: str) -> bool: diff --git a/tests/localthings/test_config_flow.py b/tests/localthings/test_config_flow.py index 6808b7d..bafeebc 100644 --- a/tests/localthings/test_config_flow.py +++ b/tests/localthings/test_config_flow.py @@ -105,6 +105,38 @@ def test_find_live_ports_detects_silent_port(socket_enabled) -> None: assert result == [live_port] +def test_find_live_ports_rescues_preferred_ports_the_sweep_missed( + socket_enabled, +) -> None: + """Issue #192: a segregated VLAN made the ICMP-based sweep call three + closed ports live while missing the one port (49154, a historically + confirmed DTLS port) that nmap showed as genuinely open|filtered. The + sweep's verdict on a preferred port shouldn't be trusted blindly -- + it must always come back as a candidate even if the sweep marked it + dead, so the config flow gets a real handshake attempt against it.""" + import socket + + from custom_components.localthings.config_flow import _find_live_ports + + # Bind and immediately close a socket on 49154 so loopback refuses + # datagrams to it -- standing in for the sweep wrongly ruling out a + # port we have strong prior evidence for. + reserved = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + reserved.bind(('127.0.0.1', 49154)) + reserved.close() + + live_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + live_sock.bind(('127.0.0.1', 0)) + live_port = live_sock.getsockname()[1] + + try: + result = _find_live_ports('127.0.0.1', [49154, live_port], 0.8) + finally: + live_sock.close() + + assert set(result) == {49154, live_port} + + async def test_probe_uses_discovered_low_port(hass: HomeAssistant, monkeypatch) -> None: """A device that only answers on 49153 — outside the historical 49154/49155 pair — is found by the liveness sweep and its port is stored