Skip to content

Commit 0a6fa97

Browse files
Add timeout to dnsip (to handle stale connections) (home-assistant#153086)
1 parent dc02002 commit 0a6fa97

File tree

3 files changed

+89
-4
lines changed

3 files changed

+89
-4
lines changed

homeassistant/components/dnsip/sensor.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
from datetime import timedelta
67
from ipaddress import IPv4Address, IPv6Address
78
import logging
@@ -88,8 +89,8 @@ def __init__(
8889
self._attr_name = "IPv6" if ipv6 else None
8990
self._attr_unique_id = f"{hostname}_{ipv6}"
9091
self.hostname = hostname
91-
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
92-
self.resolver.nameservers = [resolver]
92+
self.port = port
93+
self._resolver = resolver
9394
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
9495
self._retries = DEFAULT_RETRIES
9596
self._attr_extra_state_attributes = {
@@ -103,14 +104,26 @@ def __init__(
103104
model=aiodns.__version__,
104105
name=name,
105106
)
107+
self.resolver: aiodns.DNSResolver
108+
self.create_dns_resolver()
109+
110+
def create_dns_resolver(self) -> None:
111+
"""Create the DNS resolver."""
112+
self.resolver = aiodns.DNSResolver(tcp_port=self.port, udp_port=self.port)
113+
self.resolver.nameservers = [self._resolver]
106114

107115
async def async_update(self) -> None:
108116
"""Get the current DNS IP address for hostname."""
117+
if self.resolver._closed: # noqa: SLF001
118+
self.create_dns_resolver()
119+
response = None
109120
try:
110-
response = await self.resolver.query(self.hostname, self.querytype)
121+
async with asyncio.timeout(10):
122+
response = await self.resolver.query(self.hostname, self.querytype)
123+
except TimeoutError:
124+
await self.resolver.close()
111125
except DNSError as err:
112126
_LOGGER.warning("Exception while resolving host: %s", err)
113-
response = None
114127

115128
if response:
116129
sorted_ips = sort_ips(

tests/components/dnsip/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(
2323
self.nameservers = nameservers
2424
self._nameservers = ["1.2.3.4"]
2525
self.error = error
26+
self._closed = False
2627

2728
async def query(self, hostname, qtype) -> list[QueryResult]:
2829
"""Return information."""
@@ -47,3 +48,7 @@ def nameservers(self) -> list[str]:
4748
@nameservers.setter
4849
def nameservers(self, value: list[str]) -> None:
4950
self._nameservers = value
51+
52+
async def close(self) -> None:
53+
"""Close the resolver."""
54+
self._closed = True

tests/components/dnsip/test_sensor.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,70 @@ async def test_sensor_no_response(
171171

172172
state = hass.states.get("sensor.home_assistant_io")
173173
assert state.state == STATE_UNAVAILABLE
174+
175+
176+
async def test_sensor_timeout(
177+
hass: HomeAssistant, freezer: FrozenDateTimeFactory
178+
) -> None:
179+
"""Test the DNS IP sensor with timeout."""
180+
entry = MockConfigEntry(
181+
domain=DOMAIN,
182+
source=SOURCE_USER,
183+
data={
184+
CONF_HOSTNAME: "home-assistant.io",
185+
CONF_NAME: "home-assistant.io",
186+
CONF_IPV4: True,
187+
CONF_IPV6: False,
188+
},
189+
options={
190+
CONF_RESOLVER: "208.67.222.222",
191+
CONF_RESOLVER_IPV6: "2620:119:53::53",
192+
CONF_PORT: 53,
193+
CONF_PORT_IPV6: 53,
194+
},
195+
entry_id="1",
196+
unique_id="home-assistant.io",
197+
)
198+
entry.add_to_hass(hass)
199+
200+
dns_mock = RetrieveDNS()
201+
with patch(
202+
"homeassistant.components.dnsip.sensor.aiodns.DNSResolver",
203+
return_value=dns_mock,
204+
):
205+
await hass.config_entries.async_setup(entry.entry_id)
206+
await hass.async_block_till_done()
207+
208+
state = hass.states.get("sensor.home_assistant_io")
209+
210+
assert state.state == "1.1.1.1"
211+
212+
with (
213+
patch(
214+
"homeassistant.components.dnsip.sensor.aiodns.DNSResolver",
215+
return_value=dns_mock,
216+
),
217+
patch(
218+
"homeassistant.components.dnsip.sensor.asyncio.timeout",
219+
side_effect=TimeoutError(),
220+
),
221+
):
222+
freezer.tick(timedelta(seconds=SCAN_INTERVAL.seconds))
223+
async_fire_time_changed(hass)
224+
await hass.async_block_till_done()
225+
226+
# Allows 2 retries before going unavailable
227+
state = hass.states.get("sensor.home_assistant_io")
228+
assert state.state == "1.1.1.1"
229+
assert state.attributes["ip_addresses"] == ["1.1.1.1", "1.2.3.4"]
230+
231+
freezer.tick(timedelta(seconds=SCAN_INTERVAL.seconds))
232+
async_fire_time_changed(hass)
233+
await hass.async_block_till_done()
234+
235+
freezer.tick(timedelta(seconds=SCAN_INTERVAL.seconds))
236+
async_fire_time_changed(hass)
237+
await hass.async_block_till_done()
238+
239+
state = hass.states.get("sensor.home_assistant_io")
240+
assert state.state == STATE_UNAVAILABLE

0 commit comments

Comments
 (0)