22
33from __future__ import annotations
44
5- from collections .abc import Callable , Coroutine , Sequence
6- from datetime import datetime , timedelta
75import logging
8- from typing import Any , cast
96
10- from aiohttp import ClientSession
117import voluptuous as vol
128
13- from homeassistant .config_entries import SOURCE_IMPORT , ConfigEntry
9+ from homeassistant .config_entries import SOURCE_IMPORT
1410from homeassistant .const import CONF_ACCESS_TOKEN , CONF_DOMAIN
15- from homeassistant .core import (
16- CALLBACK_TYPE ,
17- HassJob ,
18- HomeAssistant ,
19- ServiceCall ,
20- callback ,
21- )
11+ from homeassistant .core import HomeAssistant , ServiceCall
2212from homeassistant .exceptions import ServiceValidationError
2313from homeassistant .helpers import config_validation as cv
2414from homeassistant .helpers .aiohttp_client import async_get_clientsession
25- from homeassistant .helpers .event import async_call_later
2615from homeassistant .helpers .selector import ConfigEntrySelector
2716from homeassistant .helpers .typing import ConfigType
28- from homeassistant .loader import bind_hass
29- from homeassistant .util import dt as dt_util
3017
3118from .const import ATTR_CONFIG_ENTRY
19+ from .coordinator import DuckDnsConfigEntry , DuckDnsUpdateCoordinator
20+ from .helpers import update_duckdns
3221
3322_LOGGER = logging .getLogger (__name__ )
3423
3524ATTR_TXT = "txt"
3625
3726DOMAIN = "duckdns"
3827
39- INTERVAL = timedelta (minutes = 5 )
40- BACKOFF_INTERVALS = (
41- INTERVAL ,
42- timedelta (minutes = 1 ),
43- timedelta (minutes = 5 ),
44- timedelta (minutes = 15 ),
45- timedelta (minutes = 30 ),
46- )
4728SERVICE_SET_TXT = "set_txt"
4829
49- UPDATE_URL = "https://www.duckdns.org/update"
5030
5131CONFIG_SCHEMA = vol .Schema (
5232 {
7151 }
7252)
7353
74- type DuckDnsConfigEntry = ConfigEntry
75-
7654
7755async def async_setup (hass : HomeAssistant , config : ConfigType ) -> bool :
7856 """Initialize the DuckDNS component."""
@@ -99,21 +77,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
9977async def async_setup_entry (hass : HomeAssistant , entry : DuckDnsConfigEntry ) -> bool :
10078 """Set up Duck DNS from a config entry."""
10179
102- session = async_get_clientsession (hass )
80+ coordinator = DuckDnsUpdateCoordinator (hass , entry )
81+ await coordinator .async_config_entry_first_refresh ()
82+ entry .runtime_data = coordinator
10383
104- async def update_domain_interval (_now : datetime ) -> bool :
105- """Update the DuckDNS entry."""
106- return await _update_duckdns (
107- session ,
108- entry .data [CONF_DOMAIN ],
109- entry .data [CONF_ACCESS_TOKEN ],
110- )
111-
112- entry .async_on_unload (
113- async_track_time_interval_backoff (
114- hass , update_domain_interval , BACKOFF_INTERVALS
115- )
116- )
84+ # Add a dummy listener as we do not have regular entities
85+ entry .async_on_unload (coordinator .async_add_listener (lambda : None ))
11786
11887 return True
11988
@@ -153,7 +122,7 @@ async def update_domain_service(call: ServiceCall) -> None:
153122
154123 session = async_get_clientsession (call .hass )
155124
156- await _update_duckdns (
125+ await update_duckdns (
157126 session ,
158127 entry .data [CONF_DOMAIN ],
159128 entry .data [CONF_ACCESS_TOKEN ],
@@ -164,73 +133,3 @@ async def update_domain_service(call: ServiceCall) -> None:
164133async def async_unload_entry (hass : HomeAssistant , entry : DuckDnsConfigEntry ) -> bool :
165134 """Unload a config entry."""
166135 return True
167-
168-
169- _SENTINEL = object ()
170-
171-
172- async def _update_duckdns (
173- session : ClientSession ,
174- domain : str ,
175- token : str ,
176- * ,
177- txt : str | None | object = _SENTINEL ,
178- clear : bool = False ,
179- ) -> bool :
180- """Update DuckDNS."""
181- params = {"domains" : domain , "token" : token }
182-
183- if txt is not _SENTINEL :
184- if txt is None :
185- # Pass in empty txt value to indicate it's clearing txt record
186- params ["txt" ] = ""
187- clear = True
188- else :
189- params ["txt" ] = cast (str , txt )
190-
191- if clear :
192- params ["clear" ] = "true"
193-
194- resp = await session .get (UPDATE_URL , params = params )
195- body = await resp .text ()
196-
197- if body != "OK" :
198- _LOGGER .warning ("Updating DuckDNS domain failed: %s" , domain )
199- return False
200-
201- return True
202-
203-
204- @callback
205- @bind_hass
206- def async_track_time_interval_backoff (
207- hass : HomeAssistant ,
208- action : Callable [[datetime ], Coroutine [Any , Any , bool ]],
209- intervals : Sequence [timedelta ],
210- ) -> CALLBACK_TYPE :
211- """Add a listener that fires repetitively at every timedelta interval."""
212- remove : CALLBACK_TYPE | None = None
213- failed = 0
214-
215- async def interval_listener (now : datetime ) -> None :
216- """Handle elapsed intervals with backoff."""
217- nonlocal failed , remove
218- try :
219- failed += 1
220- if await action (now ):
221- failed = 0
222- finally :
223- delay = intervals [failed ] if failed < len (intervals ) else intervals [- 1 ]
224- remove = async_call_later (
225- hass , delay .total_seconds (), interval_listener_job
226- )
227-
228- interval_listener_job = HassJob (interval_listener , cancel_on_shutdown = True )
229- hass .async_run_hass_job (interval_listener_job , dt_util .utcnow ())
230-
231- def remove_listener () -> None :
232- """Remove interval listener."""
233- if remove :
234- remove ()
235-
236- return remove_listener
0 commit comments