1
0
mirror of https://github.com/home-assistant/core.git synced 2026-03-02 07:29:28 +00:00

Add timeout to dnsip (to handle stale connections) (#153086)

This commit is contained in:
G Johansson
2025-09-29 14:49:38 +02:00
committed by GitHub
parent dc02002b9d
commit 0a6fa978fa
3 changed files with 89 additions and 4 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from datetime import timedelta
from ipaddress import IPv4Address, IPv6Address
import logging
@@ -88,8 +89,8 @@ class WanIpSensor(SensorEntity):
self._attr_name = "IPv6" if ipv6 else None
self._attr_unique_id = f"{hostname}_{ipv6}"
self.hostname = hostname
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
self.resolver.nameservers = [resolver]
self.port = port
self._resolver = resolver
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
self._retries = DEFAULT_RETRIES
self._attr_extra_state_attributes = {
@@ -103,14 +104,26 @@ class WanIpSensor(SensorEntity):
model=aiodns.__version__,
name=name,
)
self.resolver: aiodns.DNSResolver
self.create_dns_resolver()
def create_dns_resolver(self) -> None:
"""Create the DNS resolver."""
self.resolver = aiodns.DNSResolver(tcp_port=self.port, udp_port=self.port)
self.resolver.nameservers = [self._resolver]
async def async_update(self) -> None:
"""Get the current DNS IP address for hostname."""
if self.resolver._closed: # noqa: SLF001
self.create_dns_resolver()
response = None
try:
response = await self.resolver.query(self.hostname, self.querytype)
async with asyncio.timeout(10):
response = await self.resolver.query(self.hostname, self.querytype)
except TimeoutError:
await self.resolver.close()
except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err)
response = None
if response:
sorted_ips = sort_ips(

View File

@@ -23,6 +23,7 @@ class RetrieveDNS:
self.nameservers = nameservers
self._nameservers = ["1.2.3.4"]
self.error = error
self._closed = False
async def query(self, hostname, qtype) -> list[QueryResult]:
"""Return information."""
@@ -47,3 +48,7 @@ class RetrieveDNS:
@nameservers.setter
def nameservers(self, value: list[str]) -> None:
self._nameservers = value
async def close(self) -> None:
"""Close the resolver."""
self._closed = True

View File

@@ -171,3 +171,70 @@ async def test_sensor_no_response(
state = hass.states.get("sensor.home_assistant_io")
assert state.state == STATE_UNAVAILABLE
async def test_sensor_timeout(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Test the DNS IP sensor with timeout."""
entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
data={
CONF_HOSTNAME: "home-assistant.io",
CONF_NAME: "home-assistant.io",
CONF_IPV4: True,
CONF_IPV6: False,
},
options={
CONF_RESOLVER: "208.67.222.222",
CONF_RESOLVER_IPV6: "2620:119:53::53",
CONF_PORT: 53,
CONF_PORT_IPV6: 53,
},
entry_id="1",
unique_id="home-assistant.io",
)
entry.add_to_hass(hass)
dns_mock = RetrieveDNS()
with patch(
"homeassistant.components.dnsip.sensor.aiodns.DNSResolver",
return_value=dns_mock,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.home_assistant_io")
assert state.state == "1.1.1.1"
with (
patch(
"homeassistant.components.dnsip.sensor.aiodns.DNSResolver",
return_value=dns_mock,
),
patch(
"homeassistant.components.dnsip.sensor.asyncio.timeout",
side_effect=TimeoutError(),
),
):
freezer.tick(timedelta(seconds=SCAN_INTERVAL.seconds))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Allows 2 retries before going unavailable
state = hass.states.get("sensor.home_assistant_io")
assert state.state == "1.1.1.1"
assert state.attributes["ip_addresses"] == ["1.1.1.1", "1.2.3.4"]
freezer.tick(timedelta(seconds=SCAN_INTERVAL.seconds))
async_fire_time_changed(hass)
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=SCAN_INTERVAL.seconds))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.home_assistant_io")
assert state.state == STATE_UNAVAILABLE