1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Add config flow to Duck DNS integration (#147693)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Manu
2025-11-23 15:54:51 +01:00
committed by GitHub
parent f8bf7ec1ff
commit 5916af1115
13 changed files with 666 additions and 68 deletions

2
CODEOWNERS generated
View File

@@ -391,6 +391,8 @@ build.json @home-assistant/supervisor
/tests/components/dsmr/ @Robbie1221
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duckdns/ @tr4nt0r
/tests/components/duckdns/ @tr4nt0r
/homeassistant/components/duke_energy/ @hunterjm
/tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd

View File

@@ -10,6 +10,7 @@ from typing import Any, cast
from aiohttp import ClientSession
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import (
CALLBACK_TYPE,
@@ -18,13 +19,17 @@ from homeassistant.core import (
ServiceCall,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from .const import ATTR_CONFIG_ENTRY
_LOGGER = logging.getLogger(__name__)
ATTR_TXT = "txt"
@@ -32,7 +37,13 @@ ATTR_TXT = "txt"
DOMAIN = "duckdns"
INTERVAL = timedelta(minutes=5)
BACKOFF_INTERVALS = (
INTERVAL,
timedelta(minutes=1),
timedelta(minutes=5),
timedelta(minutes=15),
timedelta(minutes=30),
)
SERVICE_SET_TXT = "set_txt"
UPDATE_URL = "https://www.duckdns.org/update"
@@ -49,36 +60,109 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
SERVICE_TXT_SCHEMA = vol.Schema({vol.Required(ATTR_TXT): vol.Any(None, cv.string)})
SERVICE_TXT_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_CONFIG_ENTRY): ConfigEntrySelector(
{
"integration": DOMAIN,
}
),
vol.Optional(ATTR_TXT): vol.Any(None, cv.string),
}
)
type DuckDnsConfigEntry = ConfigEntry
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]
hass.services.async_register(
DOMAIN,
SERVICE_SET_TXT,
update_domain_service,
schema=SERVICE_TXT_SCHEMA,
)
if DOMAIN not in config:
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
"""Set up Duck DNS from a config entry."""
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),
return await _update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
)
async_track_time_interval_backoff(hass, update_domain_interval, intervals)
async def update_domain_service(call: ServiceCall) -> None:
entry.async_on_unload(
async_track_time_interval_backoff(
hass, update_domain_interval, BACKOFF_INTERVALS
)
)
return True
def get_config_entry(
hass: HomeAssistant, entry_id: str | None = None
) -> DuckDnsConfigEntry:
"""Return config entry or raise if not found or not loaded."""
if entry_id is None:
if not (config_entries := hass.config_entries.async_entries(DOMAIN)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_found",
)
if len(config_entries) != 1:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_selected",
)
return config_entries[0]
if not (entry := hass.config_entries.async_get_entry(entry_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_found",
)
return entry
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
entry = get_config_entry(call.hass, call.data.get(ATTR_CONFIG_ENTRY))
session = async_get_clientsession(call.hass)
await _update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
txt=call.data.get(ATTR_TXT),
)
async def async_unload_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
"""Unload a config entry."""
return True

View File

@@ -0,0 +1,81 @@
"""Config flow for the Duck DNS integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from . import _update_duckdns
from .const import DOMAIN
from .issue import deprecate_yaml_issue
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_DOMAIN): TextSelector(
TextSelectorConfig(type=TextSelectorType.TEXT, suffix=".duckdns.org")
),
vol.Required(CONF_ACCESS_TOKEN): str,
}
)
class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Duck DNS."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_DOMAIN: user_input[CONF_DOMAIN]})
session = async_get_clientsession(self.hass)
try:
if not await _update_duckdns(
session,
user_input[CONF_DOMAIN],
user_input[CONF_ACCESS_TOKEN],
):
errors["base"] = "update_failed"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
return self.async_create_entry(
title=f"{user_input[CONF_DOMAIN]}.duckdns.org", data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input
),
errors=errors,
description_placeholders={"url": "https://www.duckdns.org/"},
)
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
"""Import config from yaml."""
self._async_abort_entries_match({CONF_DOMAIN: import_info[CONF_DOMAIN]})
result = await self.async_step_user(import_info)
if errors := result.get("errors"):
deprecate_yaml_issue(self.hass, import_success=False)
return self.async_abort(reason=errors["base"])
deprecate_yaml_issue(self.hass, import_success=True)
return result

View File

@@ -0,0 +1,7 @@
"""Constants for the Duck DNS integration."""
from typing import Final
DOMAIN = "duckdns"
ATTR_CONFIG_ENTRY: Final = "config_entry_id"

View File

@@ -0,0 +1,40 @@
"""Issues for Duck DNS integration."""
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN
@callback
def deprecate_yaml_issue(hass: HomeAssistant, *, import_success: bool) -> None:
"""Deprecate yaml issue."""
if import_success:
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
is_fixable=False,
issue_domain=DOMAIN,
breaks_in_ha_version="2026.6.0",
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Duck DNS",
},
)
else:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_import_issue_error",
breaks_in_ha_version="2026.6.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_error",
translation_placeholders={
"url": "/config/integrations/dashboard/add?domain=duckdns"
},
)

View File

@@ -1,8 +1,8 @@
{
"domain": "duckdns",
"name": "Duck DNS",
"codeowners": [],
"codeowners": ["@tr4nt0r"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/duckdns",
"iot_class": "cloud_polling",
"quality_scale": "legacy"
"iot_class": "cloud_polling"
}

View File

@@ -1,7 +1,10 @@
set_txt:
fields:
config_entry_id:
selector:
config_entry:
integration: duckdns
txt:
required: true
example: "This domain name is reserved for use in documentation"
selector:
text:

View File

@@ -1,8 +1,48 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"unknown": "[%key:common::config_flow::error::unknown%]",
"update_failed": "Updating DuckDNS failed"
},
"step": {
"user": {
"data": {
"access_token": "Token",
"domain": "Subdomain"
},
"data_description": {
"access_token": "Your Duck DNS account token",
"domain": "The Duck DNS subdomain to update"
},
"description": "Enter your Duck DNS subdomain and token below to configure dynamic DNS updates. You can find your token on the [Duck DNS]({url}) homepage after logging into your account."
}
}
},
"exceptions": {
"entry_not_found": {
"message": "Duck DNS integration entry not found"
},
"entry_not_selected": {
"message": "Duck DNS integration entry not selected"
}
},
"issues": {
"deprecated_yaml_import_issue_error": {
"description": "Configuring Duck DNS using YAML is being removed but there was an error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the Duck DNS YAML configuration from your `configuration.yaml` file and continue to [set up the integration]({url}) manually.",
"title": "The Duck DNS YAML configuration import failed"
}
},
"services": {
"set_txt": {
"description": "Sets the TXT record of your DuckDNS subdomain.",
"fields": {
"config_entry_id": {
"description": "The Duck DNS integration ID.",
"name": "Integration ID"
},
"txt": {
"description": "Payload for the TXT record.",
"name": "TXT"

View File

@@ -157,6 +157,7 @@ FLOWS = {
"droplet",
"dsmr",
"dsmr_reader",
"duckdns",
"duke_energy",
"dunehd",
"duotecno",

View File

@@ -1467,7 +1467,7 @@
"duckdns": {
"name": "Duck DNS",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "cloud_polling"
},
"duke_energy": {

View File

@@ -0,0 +1,48 @@
"""Common fixtures for the Duck DNS tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.duckdns.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from tests.common import MockConfigEntry
TEST_SUBDOMAIN = "homeassistant"
TEST_TOKEN = "123e4567-e89b-12d3-a456-426614174000"
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.duckdns.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(name="config_entry")
def mock_config_entry() -> MockConfigEntry:
"""Mock Duck DNS configuration entry."""
return MockConfigEntry(
domain=DOMAIN,
title=f"{TEST_SUBDOMAIN}.duckdns.org",
data={
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
entry_id="12345",
)
@pytest.fixture
def mock_update_duckdns() -> Generator[AsyncMock]:
"""Mock _update_duckdns."""
with patch(
"homeassistant.components.duckdns.config_flow._update_duckdns",
return_value=True,
) as mock:
yield mock

View File

@@ -0,0 +1,255 @@
"""Test the Duck DNS config flow."""
from unittest.mock import AsyncMock
import pytest
from homeassistant.components.duckdns.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from .conftest import TEST_SUBDOMAIN, TEST_TOKEN
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_update_duckdns")
async def test_form(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: "123e4567-e89b-12d3-a456-426614174000",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
assert result["data"] == {
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_update_duckdns")
async def test_form_already_configured(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test we abort if already configured."""
config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: "123e4567-e89b-12d3-a456-426614174000",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_form_unknown_exception(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_update_duckdns: AsyncMock,
) -> None:
"""Test we handle unknown exception."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_update_duckdns.side_effect = ValueError
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "unknown"}
mock_update_duckdns.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
assert result["data"] == {
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_update_failed(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_update_duckdns: AsyncMock,
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_update_duckdns.return_value = False
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "update_failed"}
mock_update_duckdns.return_value = True
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
assert result["data"] == {
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_update_duckdns")
async def test_import(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test import flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{TEST_SUBDOMAIN}.duckdns.org"
assert result["data"] == {
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
}
assert len(mock_setup_entry.mock_calls) == 1
assert issue_registry.async_get_issue(
domain=HOMEASSISTANT_DOMAIN,
issue_id=f"deprecated_yaml_{DOMAIN}",
)
async def test_import_failed(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
issue_registry: ir.IssueRegistry,
mock_update_duckdns: AsyncMock,
) -> None:
"""Test import flow failed."""
mock_update_duckdns.return_value = False
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "update_failed"
assert len(mock_setup_entry.mock_calls) == 0
assert issue_registry.async_get_issue(
domain=DOMAIN,
issue_id="deprecated_yaml_import_issue_error",
)
async def test_import_exception(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
issue_registry: ir.IssueRegistry,
mock_update_duckdns: AsyncMock,
) -> None:
"""Test import flow failed unknown."""
mock_update_duckdns.side_effect = ValueError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_DOMAIN: TEST_SUBDOMAIN,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unknown"
assert len(mock_setup_entry.mock_calls) == 0
assert issue_registry.async_get_issue(
domain=DOMAIN,
issue_id="deprecated_yaml_import_issue_error",
)
@pytest.mark.usefixtures("mock_update_duckdns")
async def test_init_import_flow(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test yaml triggers import flow."""
await async_setup_component(
hass,
DOMAIN,
{"duckdns": {CONF_DOMAIN: TEST_SUBDOMAIN, CONF_ACCESS_TOKEN: TEST_TOKEN}},
)
assert len(mock_setup_entry.mock_calls) == 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 1

View File

@@ -5,19 +5,25 @@ import logging
import pytest
from homeassistant.components import duckdns
from homeassistant.components.duckdns import async_track_time_interval_backoff
from homeassistant.components.duckdns import (
ATTR_TXT,
BACKOFF_INTERVALS,
DOMAIN,
INTERVAL,
SERVICE_SET_TXT,
UPDATE_URL,
async_track_time_interval_backoff,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed
from .conftest import TEST_SUBDOMAIN, TEST_TOKEN
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
DOMAIN = "bla"
TOKEN = "abcdefgh"
_LOGGER = logging.getLogger(__name__)
INTERVAL = duckdns.INTERVAL
async def async_set_txt(hass: HomeAssistant, txt: str | None) -> None:
@@ -26,37 +32,40 @@ async def async_set_txt(hass: HomeAssistant, txt: str | None) -> None:
This is a legacy helper method. Do not use it for new tests.
"""
await hass.services.async_call(
duckdns.DOMAIN, duckdns.SERVICE_SET_TXT, {duckdns.ATTR_TXT: txt}, blocking=True
DOMAIN, SERVICE_SET_TXT, {ATTR_TXT: txt}, blocking=True
)
@pytest.fixture
async def setup_duckdns(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry: MockConfigEntry,
) -> None:
"""Fixture that sets up DuckDNS."""
aioclient_mock.get(
duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="OK"
UPDATE_URL,
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
text="OK",
)
await async_setup_component(
hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}}
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
@pytest.mark.usefixtures("setup_duckdns")
async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
"""Test setup works if update passes."""
aioclient_mock.get(
duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="OK"
UPDATE_URL,
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
text="OK",
)
result = await async_setup_component(
hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}}
)
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 1
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
@@ -65,50 +74,47 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -
async def test_setup_backoff(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry: MockConfigEntry,
) -> None:
"""Test setup fails if first update fails."""
aioclient_mock.get(
duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="KO"
UPDATE_URL,
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
text="KO",
)
result = await async_setup_component(
hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}}
)
assert result
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert aioclient_mock.call_count == 1
# Copy of the DuckDNS intervals from duckdns/__init__.py
intervals = (
INTERVAL,
timedelta(minutes=1),
timedelta(minutes=5),
timedelta(minutes=15),
timedelta(minutes=30),
)
tme = utcnow()
await hass.async_block_till_done()
_LOGGER.debug("Backoff")
for idx in range(1, len(intervals)):
tme += intervals[idx]
for idx in range(1, len(BACKOFF_INTERVALS)):
tme += BACKOFF_INTERVALS[idx]
async_fire_time_changed(hass, tme)
await hass.async_block_till_done()
assert aioclient_mock.call_count == idx + 1
@pytest.mark.usefixtures("setup_duckdns")
async def test_service_set_txt(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_duckdns
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test set txt service call."""
# Empty the fixture mock requests
aioclient_mock.clear_requests()
aioclient_mock.get(
duckdns.UPDATE_URL,
params={"domains": DOMAIN, "token": TOKEN, "txt": "some-txt"},
UPDATE_URL,
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN, "txt": "some-txt"},
text="OK",
)
@@ -117,16 +123,22 @@ async def test_service_set_txt(
assert aioclient_mock.call_count == 1
@pytest.mark.usefixtures("setup_duckdns")
async def test_service_clear_txt(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_duckdns
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test clear txt service call."""
# Empty the fixture mock requests
aioclient_mock.clear_requests()
aioclient_mock.get(
duckdns.UPDATE_URL,
params={"domains": DOMAIN, "token": TOKEN, "txt": "", "clear": "true"},
UPDATE_URL,
params={
"domains": TEST_SUBDOMAIN,
"token": TEST_TOKEN,
"txt": "",
"clear": "true",
},
text="OK",
)
@@ -194,3 +206,28 @@ async def test_async_track_time_interval_backoff(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert call_count == _idx
async def test_load_unload(
hass: HomeAssistant,
config_entry: MockConfigEntry,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test loading and unloading of the config entry."""
aioclient_mock.get(
UPDATE_URL,
params={"domains": TEST_SUBDOMAIN, "token": TEST_TOKEN},
text="OK",
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert config_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.NOT_LOADED