|
10 | 10 | from aiohttp import ClientSession |
11 | 11 | import voluptuous as vol |
12 | 12 |
|
| 13 | +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry |
13 | 14 | from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN |
14 | 15 | from homeassistant.core import ( |
15 | 16 | CALLBACK_TYPE, |
|
18 | 19 | ServiceCall, |
19 | 20 | callback, |
20 | 21 | ) |
| 22 | +from homeassistant.exceptions import ServiceValidationError |
21 | 23 | from homeassistant.helpers import config_validation as cv |
22 | 24 | from homeassistant.helpers.aiohttp_client import async_get_clientsession |
23 | 25 | from homeassistant.helpers.event import async_call_later |
| 26 | +from homeassistant.helpers.selector import ConfigEntrySelector |
24 | 27 | from homeassistant.helpers.typing import ConfigType |
25 | 28 | from homeassistant.loader import bind_hass |
26 | 29 | from homeassistant.util import dt as dt_util |
27 | 30 |
|
| 31 | +from .const import ATTR_CONFIG_ENTRY |
| 32 | + |
28 | 33 | _LOGGER = logging.getLogger(__name__) |
29 | 34 |
|
30 | 35 | ATTR_TXT = "txt" |
31 | 36 |
|
32 | 37 | DOMAIN = "duckdns" |
33 | 38 |
|
34 | 39 | INTERVAL = timedelta(minutes=5) |
35 | | - |
| 40 | +BACKOFF_INTERVALS = ( |
| 41 | + INTERVAL, |
| 42 | + timedelta(minutes=1), |
| 43 | + timedelta(minutes=5), |
| 44 | + timedelta(minutes=15), |
| 45 | + timedelta(minutes=30), |
| 46 | +) |
36 | 47 | SERVICE_SET_TXT = "set_txt" |
37 | 48 |
|
38 | 49 | UPDATE_URL = "https://www.duckdns.org/update" |
|
49 | 60 | extra=vol.ALLOW_EXTRA, |
50 | 61 | ) |
51 | 62 |
|
52 | | -SERVICE_TXT_SCHEMA = vol.Schema({vol.Required(ATTR_TXT): vol.Any(None, cv.string)}) |
| 63 | +SERVICE_TXT_SCHEMA = vol.Schema( |
| 64 | + { |
| 65 | + vol.Optional(ATTR_CONFIG_ENTRY): ConfigEntrySelector( |
| 66 | + { |
| 67 | + "integration": DOMAIN, |
| 68 | + } |
| 69 | + ), |
| 70 | + vol.Optional(ATTR_TXT): vol.Any(None, cv.string), |
| 71 | + } |
| 72 | +) |
| 73 | + |
| 74 | +type DuckDnsConfigEntry = ConfigEntry |
53 | 75 |
|
54 | 76 |
|
55 | 77 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: |
56 | 78 | """Initialize the DuckDNS component.""" |
57 | | - domain: str = config[DOMAIN][CONF_DOMAIN] |
58 | | - token: str = config[DOMAIN][CONF_ACCESS_TOKEN] |
| 79 | + |
| 80 | + hass.services.async_register( |
| 81 | + DOMAIN, |
| 82 | + SERVICE_SET_TXT, |
| 83 | + update_domain_service, |
| 84 | + schema=SERVICE_TXT_SCHEMA, |
| 85 | + ) |
| 86 | + |
| 87 | + if DOMAIN not in config: |
| 88 | + return True |
| 89 | + |
| 90 | + hass.async_create_task( |
| 91 | + hass.config_entries.flow.async_init( |
| 92 | + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] |
| 93 | + ) |
| 94 | + ) |
| 95 | + |
| 96 | + return True |
| 97 | + |
| 98 | + |
| 99 | +async def async_setup_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool: |
| 100 | + """Set up Duck DNS from a config entry.""" |
| 101 | + |
59 | 102 | session = async_get_clientsession(hass) |
60 | 103 |
|
61 | 104 | async def update_domain_interval(_now: datetime) -> bool: |
62 | 105 | """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), |
| 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 | + ) |
71 | 116 | ) |
72 | | - async_track_time_interval_backoff(hass, update_domain_interval, intervals) |
73 | 117 |
|
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]) |
| 118 | + return True |
77 | 119 |
|
78 | | - hass.services.async_register( |
79 | | - DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA |
| 120 | + |
| 121 | +def get_config_entry( |
| 122 | + hass: HomeAssistant, entry_id: str | None = None |
| 123 | +) -> DuckDnsConfigEntry: |
| 124 | + """Return config entry or raise if not found or not loaded.""" |
| 125 | + |
| 126 | + if entry_id is None: |
| 127 | + if not (config_entries := hass.config_entries.async_entries(DOMAIN)): |
| 128 | + raise ServiceValidationError( |
| 129 | + translation_domain=DOMAIN, |
| 130 | + translation_key="entry_not_found", |
| 131 | + ) |
| 132 | + |
| 133 | + if len(config_entries) != 1: |
| 134 | + raise ServiceValidationError( |
| 135 | + translation_domain=DOMAIN, |
| 136 | + translation_key="entry_not_selected", |
| 137 | + ) |
| 138 | + return config_entries[0] |
| 139 | + |
| 140 | + if not (entry := hass.config_entries.async_get_entry(entry_id)): |
| 141 | + raise ServiceValidationError( |
| 142 | + translation_domain=DOMAIN, |
| 143 | + translation_key="entry_not_found", |
| 144 | + ) |
| 145 | + |
| 146 | + return entry |
| 147 | + |
| 148 | + |
| 149 | +async def update_domain_service(call: ServiceCall) -> None: |
| 150 | + """Update the DuckDNS entry.""" |
| 151 | + |
| 152 | + entry = get_config_entry(call.hass, call.data.get(ATTR_CONFIG_ENTRY)) |
| 153 | + |
| 154 | + session = async_get_clientsession(call.hass) |
| 155 | + |
| 156 | + await _update_duckdns( |
| 157 | + session, |
| 158 | + entry.data[CONF_DOMAIN], |
| 159 | + entry.data[CONF_ACCESS_TOKEN], |
| 160 | + txt=call.data.get(ATTR_TXT), |
80 | 161 | ) |
81 | 162 |
|
| 163 | + |
| 164 | +async def async_unload_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool: |
| 165 | + """Unload a config entry.""" |
82 | 166 | return True |
83 | 167 |
|
84 | 168 |
|
|
0 commit comments