Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Integrate with DuckDNS."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine, Sequence
6 from datetime import datetime, timedelta
7 import logging
8 from typing import Any, cast
9 
10 from aiohttp import ClientSession
11 import voluptuous as vol
12 
13 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
14 from homeassistant.core import (
15  CALLBACK_TYPE,
16  HassJob,
17  HomeAssistant,
18  ServiceCall,
19  callback,
20 )
21 from homeassistant.helpers.aiohttp_client import async_get_clientsession
23 from homeassistant.helpers.event import async_call_later
24 from homeassistant.helpers.typing import ConfigType
25 from homeassistant.loader import bind_hass
26 from homeassistant.util import dt as dt_util
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 ATTR_TXT = "txt"
31 
32 DOMAIN = "duckdns"
33 
34 INTERVAL = timedelta(minutes=5)
35 
36 SERVICE_SET_TXT = "set_txt"
37 
38 UPDATE_URL = "https://www.duckdns.org/update"
39 
40 CONFIG_SCHEMA = vol.Schema(
41  {
42  DOMAIN: vol.Schema(
43  {
44  vol.Required(CONF_DOMAIN): cv.string,
45  vol.Required(CONF_ACCESS_TOKEN): cv.string,
46  }
47  )
48  },
49  extra=vol.ALLOW_EXTRA,
50 )
51 
52 SERVICE_TXT_SCHEMA = vol.Schema({vol.Required(ATTR_TXT): vol.Any(None, cv.string)})
53 
54 
55 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
56  """Initialize the DuckDNS component."""
57  domain: str = config[DOMAIN][CONF_DOMAIN]
58  token: str = config[DOMAIN][CONF_ACCESS_TOKEN]
59  session = async_get_clientsession(hass)
60 
61  async def update_domain_interval(_now: datetime) -> bool:
62  """Update the DuckDNS entry."""
63  return await _update_duckdns(session, domain, token)
64 
65  intervals = (
66  INTERVAL,
67  timedelta(minutes=1),
68  timedelta(minutes=5),
69  timedelta(minutes=15),
70  timedelta(minutes=30),
71  )
72  async_track_time_interval_backoff(hass, update_domain_interval, intervals)
73 
74  async def update_domain_service(call: ServiceCall) -> None:
75  """Update the DuckDNS entry."""
76  await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT])
77 
78  hass.services.async_register(
79  DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA
80  )
81 
82  return True
83 
84 
85 _SENTINEL = object()
86 
87 
88 async def _update_duckdns(
89  session: ClientSession,
90  domain: str,
91  token: str,
92  *,
93  txt: str | None | object = _SENTINEL,
94  clear: bool = False,
95 ) -> bool:
96  """Update DuckDNS."""
97  params = {"domains": domain, "token": token}
98 
99  if txt is not _SENTINEL:
100  if txt is None:
101  # Pass in empty txt value to indicate it's clearing txt record
102  params["txt"] = ""
103  clear = True
104  else:
105  params["txt"] = cast(str, txt)
106 
107  if clear:
108  params["clear"] = "true"
109 
110  resp = await session.get(UPDATE_URL, params=params)
111  body = await resp.text()
112 
113  if body != "OK":
114  _LOGGER.warning("Updating DuckDNS domain failed: %s", domain)
115  return False
116 
117  return True
118 
119 
120 @callback
121 @bind_hass
123  hass: HomeAssistant,
124  action: Callable[[datetime], Coroutine[Any, Any, bool]],
125  intervals: Sequence[timedelta],
126 ) -> CALLBACK_TYPE:
127  """Add a listener that fires repetitively at every timedelta interval."""
128  remove: CALLBACK_TYPE | None = None
129  failed = 0
130 
131  async def interval_listener(now: datetime) -> None:
132  """Handle elapsed intervals with backoff."""
133  nonlocal failed, remove
134  try:
135  failed += 1
136  if await action(now):
137  failed = 0
138  finally:
139  delay = intervals[failed] if failed < len(intervals) else intervals[-1]
140  remove = async_call_later(
141  hass, delay.total_seconds(), interval_listener_job
142  )
143 
144  interval_listener_job = HassJob(interval_listener, cancel_on_shutdown=True)
145  hass.async_run_hass_job(interval_listener_job, dt_util.utcnow())
146 
147  def remove_listener() -> None:
148  """Remove interval listener."""
149  if remove:
150  remove()
151 
152  return remove_listener
bool remove(self, _T matcher)
Definition: match.py:214
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:55
bool _update_duckdns(ClientSession session, str domain, str token, *str|None|object txt=_SENTINEL, bool clear=False)
Definition: __init__.py:95
CALLBACK_TYPE async_track_time_interval_backoff(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, bool]] action, Sequence[timedelta] intervals)
Definition: __init__.py:126
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597