mirror of https://github.com/home-assistant/core
153 lines
4.1 KiB
Python
153 lines
4.1 KiB
Python
"""Integrate with DuckDNS."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Coroutine, Sequence
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
from typing import Any, cast
|
|
|
|
from aiohttp import ClientSession
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
|
|
from homeassistant.core import (
|
|
CALLBACK_TYPE,
|
|
HassJob,
|
|
HomeAssistant,
|
|
ServiceCall,
|
|
callback,
|
|
)
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.event import async_call_later
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.loader import bind_hass
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_TXT = "txt"
|
|
|
|
DOMAIN = "duckdns"
|
|
|
|
INTERVAL = timedelta(minutes=5)
|
|
|
|
SERVICE_SET_TXT = "set_txt"
|
|
|
|
UPDATE_URL = "https://www.duckdns.org/update"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_DOMAIN): cv.string,
|
|
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
SERVICE_TXT_SCHEMA = vol.Schema({vol.Required(ATTR_TXT): vol.Any(None, cv.string)})
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Initialize the DuckDNS component."""
|
|
domain: str = config[DOMAIN][CONF_DOMAIN]
|
|
token: str = config[DOMAIN][CONF_ACCESS_TOKEN]
|
|
session = async_get_clientsession(hass)
|
|
|
|
async def update_domain_interval(_now: datetime) -> bool:
|
|
"""Update the DuckDNS entry."""
|
|
return await _update_duckdns(session, domain, token)
|
|
|
|
intervals = (
|
|
INTERVAL,
|
|
timedelta(minutes=1),
|
|
timedelta(minutes=5),
|
|
timedelta(minutes=15),
|
|
timedelta(minutes=30),
|
|
)
|
|
async_track_time_interval_backoff(hass, update_domain_interval, intervals)
|
|
|
|
async def update_domain_service(call: ServiceCall) -> None:
|
|
"""Update the DuckDNS entry."""
|
|
await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT])
|
|
|
|
hass.services.async_register(
|
|
DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
_SENTINEL = object()
|
|
|
|
|
|
async def _update_duckdns(
|
|
session: ClientSession,
|
|
domain: str,
|
|
token: str,
|
|
*,
|
|
txt: str | None | object = _SENTINEL,
|
|
clear: bool = False,
|
|
) -> bool:
|
|
"""Update DuckDNS."""
|
|
params = {"domains": domain, "token": token}
|
|
|
|
if txt is not _SENTINEL:
|
|
if txt is None:
|
|
# Pass in empty txt value to indicate it's clearing txt record
|
|
params["txt"] = ""
|
|
clear = True
|
|
else:
|
|
params["txt"] = cast(str, txt)
|
|
|
|
if clear:
|
|
params["clear"] = "true"
|
|
|
|
resp = await session.get(UPDATE_URL, params=params)
|
|
body = await resp.text()
|
|
|
|
if body != "OK":
|
|
_LOGGER.warning("Updating DuckDNS domain failed: %s", domain)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
@callback
|
|
@bind_hass
|
|
def async_track_time_interval_backoff(
|
|
hass: HomeAssistant,
|
|
action: Callable[[datetime], Coroutine[Any, Any, bool]],
|
|
intervals: Sequence[timedelta],
|
|
) -> CALLBACK_TYPE:
|
|
"""Add a listener that fires repetitively at every timedelta interval."""
|
|
remove: CALLBACK_TYPE | None = None
|
|
failed = 0
|
|
|
|
async def interval_listener(now: datetime) -> None:
|
|
"""Handle elapsed intervals with backoff."""
|
|
nonlocal failed, remove
|
|
try:
|
|
failed += 1
|
|
if await action(now):
|
|
failed = 0
|
|
finally:
|
|
delay = intervals[failed] if failed < len(intervals) else intervals[-1]
|
|
remove = async_call_later(
|
|
hass, delay.total_seconds(), interval_listener_job
|
|
)
|
|
|
|
interval_listener_job = HassJob(interval_listener, cancel_on_shutdown=True)
|
|
hass.async_run_hass_job(interval_listener_job, dt_util.utcnow())
|
|
|
|
def remove_listener() -> None:
|
|
"""Remove interval listener."""
|
|
if remove:
|
|
remove()
|
|
|
|
return remove_listener
|