From af321edf8046707ce1059d0d51db64fa34edbd08 Mon Sep 17 00:00:00 2001 From: Links2004 Date: Thu, 23 Oct 2025 17:15:45 +0000 Subject: [PATCH 1/6] [core] handle mixed IP and DNS addresses correctly in resolve_ip_address do not raise error if some addresses are IPs and the mDNS / DNS resolution fails for others fix: #11501 --- esphome/helpers.py | 58 ++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index fb7b71775d..8dbbbbce11 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -250,34 +250,42 @@ def resolve_ip_address( # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: - from esphome.resolver import AsyncResolver + from esphome.core import EsphomeError - resolver = AsyncResolver(uncached_hosts, port) - addr_infos = resolver.resolve() - # Convert aioesphomeapi AddrInfo to our format - for addr_info in addr_infos: - sockaddr = addr_info.sockaddr - if addr_info.family == socket.AF_INET6: - # IPv6 - sockaddr_tuple = ( - sockaddr.address, - sockaddr.port, - sockaddr.flowinfo, - sockaddr.scope_id, + try: + from esphome.resolver import AsyncResolver + + resolver = AsyncResolver(uncached_hosts, port) + addr_infos = resolver.resolve() + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) + + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) ) + except EsphomeError as err: + if len(res) > 0: + _LOGGER.warning(err) else: - # IPv4 - sockaddr_tuple = (sockaddr.address, sockaddr.port) - - res.append( - ( - addr_info.family, - addr_info.type, - addr_info.proto, - "", # canonname - sockaddr_tuple, - ) - ) + raise err # Sort by preference res.sort(key=addr_preference_) From 8b67b9f35d0820ddebf8e784c4023038c30d0961 Mon Sep 17 00:00:00 2001 From: Links2004 Date: Thu, 23 Oct 2025 17:54:50 +0000 Subject: [PATCH 2/6] add unit tests for mixed IP and hostname resolution with proper handling of exceptions fix up address handling for mixed IP and hostname resolution --- esphome/helpers.py | 16 +++------------- tests/unit_tests/test_helpers.py | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 8dbbbbce11..986026b16d 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -224,30 +224,20 @@ def resolve_ip_address( return res # Process hosts - cached_addresses: list[str] = [] + uncached_hosts: list[str] = [] - has_cache = address_cache is not None for h in hosts: if is_ip_address(h): - if has_cache: - # If we have a cache, treat IPs as cached - cached_addresses.append(h) - else: - # If no cache, pass IPs through to resolver with hostnames - uncached_hosts.append(h) + _add_ip_addresses_to_addrinfo([h], port, res) elif address_cache and (cached := address_cache.get_addresses(h)): - # Found in cache - cached_addresses.extend(cached) + _add_ip_addresses_to_addrinfo(cached, port, res) else: # Not cached, need to resolve if address_cache and address_cache.has_cache(): _LOGGER.info("Host %s not in cache, will need to resolve", h) uncached_hosts.append(h) - # Process cached addresses (includes direct IPs and cached lookups) - _add_ip_addresses_to_addrinfo(cached_addresses, port, res) - # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: from esphome.core import EsphomeError diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 87ed901ecb..47b945e0eb 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -454,9 +454,27 @@ def test_resolve_ip_address_mixed_list() -> None: # Mix of IP and hostname - should use async resolver result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 2 + assert result[0][4][0] == "192.168.1.100" + assert result[1][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list_fail() -> None: + """Test resolving a mix of IPs and hostnames with resolve failed.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError( + "Error resolving IP address: [test.local]" + ) + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 1 - assert result[0][4][0] == "192.168.1.200" - MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) + assert result[0][4][0] == "192.168.1.100" + MockResolver.assert_called_once_with(["test.local"], 6053) mock_resolver.resolve.assert_called_once() From 3e6d1d551d8e7763763bc4be6873d24d8d594e67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:06:09 -0700 Subject: [PATCH 3/6] tweak --- esphome/helpers.py | 59 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 986026b16d..a2c6dd7d49 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -241,41 +241,42 @@ def resolve_ip_address( # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: from esphome.core import EsphomeError + from esphome.resolver import AsyncResolver + resolver = AsyncResolver(uncached_hosts, port) try: - from esphome.resolver import AsyncResolver - - resolver = AsyncResolver(uncached_hosts, port) addr_infos = resolver.resolve() - # Convert aioesphomeapi AddrInfo to our format - for addr_info in addr_infos: - sockaddr = addr_info.sockaddr - if addr_info.family == socket.AF_INET6: - # IPv6 - sockaddr_tuple = ( - sockaddr.address, - sockaddr.port, - sockaddr.flowinfo, - sockaddr.scope_id, - ) - else: - # IPv4 - sockaddr_tuple = (sockaddr.address, sockaddr.port) - - res.append( - ( - addr_info.family, - addr_info.type, - addr_info.proto, - "", # canonname - sockaddr_tuple, - ) - ) except EsphomeError as err: - if len(res) > 0: + if res: _LOGGER.warning(err) + addr_infos = [] else: - raise err + raise + + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) + + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) + ) # Sort by preference res.sort(key=addr_preference_) From 267b715bfabf933703970d7e13362ffa7cb9440b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:11:45 -0700 Subject: [PATCH 4/6] safer --- esphome/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index a2c6dd7d49..775acd0d0c 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -240,16 +240,18 @@ def resolve_ip_address( # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: + from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo + from esphome.core import EsphomeError from esphome.resolver import AsyncResolver resolver = AsyncResolver(uncached_hosts, port) + addr_infos: list[AioAddrInfo] = [] try: addr_infos = resolver.resolve() except EsphomeError as err: if res: - _LOGGER.warning(err) - addr_infos = [] + _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) else: raise From 6dab0b4b497a5e38dc36befb0d4e6fdf302ed107 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:12:57 -0700 Subject: [PATCH 5/6] tweaks --- esphome/helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 775acd0d0c..a67f2528d4 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -250,10 +250,9 @@ def resolve_ip_address( try: addr_infos = resolver.resolve() except EsphomeError as err: - if res: - _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) - else: + if not res: raise + _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) # Convert aioesphomeapi AddrInfo to our format for addr_info in addr_infos: From c76e44689565a5719ae90d5b638908e45e587eb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:14:24 -0700 Subject: [PATCH 6/6] tweaks --- esphome/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index a67f2528d4..ea6abff50a 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -251,8 +251,9 @@ def resolve_ip_address( addr_infos = resolver.resolve() except EsphomeError as err: if not res: + # No pre-resolved addresses available, DNS resolution is fatal raise - _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) + _LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res)) # Convert aioesphomeapi AddrInfo to our format for addr_info in addr_infos: