Files
core/homeassistant/components/dnsip/sensor.py
2025-09-30 10:46:59 +02:00

142 lines
4.3 KiB
Python

"""Get your own public IP address or that of any host."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from ipaddress import IPv4Address, IPv6Address
import logging
from typing import Literal
import aiodns
from aiodns.error import DNSError
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
CONF_HOSTNAME,
CONF_IPV4,
CONF_IPV6,
CONF_PORT_IPV6,
CONF_RESOLVER,
CONF_RESOLVER_IPV6,
DOMAIN,
)
DEFAULT_RETRIES = 2
MAX_RESULTS = 10
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=120)
def sort_ips(ips: list, querytype: Literal["A", "AAAA"]) -> list:
"""Join IPs into a single string."""
if querytype == "AAAA":
ips = [IPv6Address(ip) for ip in ips]
else:
ips = [IPv4Address(ip) for ip in ips]
return [str(ip) for ip in sorted(ips)][:MAX_RESULTS]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the dnsip sensor entry."""
hostname = entry.data[CONF_HOSTNAME]
name = entry.data[CONF_NAME]
nameserver_ipv4 = entry.options[CONF_RESOLVER]
nameserver_ipv6 = entry.options[CONF_RESOLVER_IPV6]
port_ipv4 = entry.options[CONF_PORT]
port_ipv6 = entry.options[CONF_PORT_IPV6]
entities = []
if entry.data[CONF_IPV4]:
entities.append(WanIpSensor(name, hostname, nameserver_ipv4, False, port_ipv4))
if entry.data[CONF_IPV6]:
entities.append(WanIpSensor(name, hostname, nameserver_ipv6, True, port_ipv6))
async_add_entities(entities, update_before_add=True)
class WanIpSensor(SensorEntity):
"""Implementation of a DNS IP sensor."""
_attr_has_entity_name = True
_attr_translation_key = "dnsip"
_unrecorded_attributes = frozenset({"resolver", "querytype", "ip_addresses"})
resolver: aiodns.DNSResolver
def __init__(
self,
name: str,
hostname: str,
nameserver: str,
ipv6: bool,
port: int,
) -> None:
"""Initialize the DNS IP sensor."""
self._attr_name = "IPv6" if ipv6 else None
self._attr_unique_id = f"{hostname}_{ipv6}"
self.hostname = hostname
self.port = port
self.nameserver = nameserver
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
self._retries = DEFAULT_RETRIES
self._attr_extra_state_attributes = {
"resolver": nameserver,
"querytype": self.querytype,
}
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, hostname)},
manufacturer="DNS",
model=aiodns.__version__,
name=name,
)
self.create_dns_resolver()
def create_dns_resolver(self) -> None:
"""Create the DNS resolver."""
self.resolver = aiodns.DNSResolver(
nameservers=[self.nameserver], tcp_port=self.port, udp_port=self.port
)
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:
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)
if response:
sorted_ips = sort_ips(
[res.host for res in response], querytype=self.querytype
)
self._attr_native_value = sorted_ips[0]
self._attr_extra_state_attributes["ip_addresses"] = sorted_ips
self._attr_available = True
self._retries = DEFAULT_RETRIES
elif self._retries > 0:
self._retries -= 1
else:
self._attr_available = False