Add DuckDNS component (#9556)

* Add DuckDNS component

* Address comments
This commit is contained in:
Paulus Schoutsen 2017-09-24 15:48:45 -07:00 committed by GitHub
parent 2486c9af35
commit fc4cd39cdd
2 changed files with 208 additions and 0 deletions

View File

@ -0,0 +1,102 @@
"""Integrate with DuckDNS."""
import asyncio
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.loader import bind_hass
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.aiohttp_client import async_get_clientsession
DOMAIN = 'duckdns'
UPDATE_URL = 'https://www.duckdns.org/update'
INTERVAL = timedelta(minutes=5)
_LOGGER = logging.getLogger(__name__)
SERVICE_SET_TXT = 'set_txt'
ATTR_TXT = 'txt'
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)
})
@bind_hass
@asyncio.coroutine
def async_set_txt(hass, txt):
"""Set the txt record. Pass in None to remove it."""
yield from hass.services.async_call(DOMAIN, SERVICE_SET_TXT, {
ATTR_TXT: txt
}, blocking=True)
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the DuckDNS component."""
domain = config[DOMAIN][CONF_DOMAIN]
token = config[DOMAIN][CONF_ACCESS_TOKEN]
session = async_get_clientsession(hass)
result = yield from _update_duckdns(session, domain, token)
if not result:
return False
@asyncio.coroutine
def update_domain_interval(now):
"""Update the DuckDNS entry."""
yield from _update_duckdns(session, domain, token)
@asyncio.coroutine
def update_domain_service(call):
"""Update the DuckDNS entry."""
yield from _update_duckdns(session, domain, token,
txt=call.data[ATTR_TXT])
async_track_time_interval(hass, update_domain_interval, INTERVAL)
hass.services.async_register(
DOMAIN, SERVICE_SET_TXT, update_domain_service,
schema=SERVICE_TXT_SCHEMA)
return result
_SENTINEL = object()
@asyncio.coroutine
def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False):
"""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'] = txt
if clear:
params['clear'] = 'true'
resp = yield from session.get(UPDATE_URL, params=params)
body = yield from resp.text()
if body != 'OK':
_LOGGER.warning('Updating DuckDNS domain %s failed', domain)
return False
return True

View File

@ -0,0 +1,106 @@
"""Test the DuckDNS component."""
import asyncio
from datetime import timedelta
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components import duckdns
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed
DOMAIN = 'bla'
TOKEN = 'abcdefgh'
@pytest.fixture
def setup_duckdns(hass, aioclient_mock):
"""Fixture that sets up DuckDNS."""
aioclient_mock.get(duckdns.UPDATE_URL, params={
'domains': DOMAIN,
'token': TOKEN
}, text='OK')
hass.loop.run_until_complete(async_setup_component(
hass, duckdns.DOMAIN, {
'duckdns': {
'domain': DOMAIN,
'access_token': TOKEN
}
}))
@asyncio.coroutine
def test_setup(hass, aioclient_mock):
"""Test setup works if update passes."""
aioclient_mock.get(duckdns.UPDATE_URL, params={
'domains': DOMAIN,
'token': TOKEN
}, text='OK')
result = yield from async_setup_component(hass, duckdns.DOMAIN, {
'duckdns': {
'domain': DOMAIN,
'access_token': TOKEN
}
})
assert result
assert aioclient_mock.call_count == 1
async_fire_time_changed(hass, utcnow() + timedelta(minutes=5))
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 2
@asyncio.coroutine
def test_setup_fails_if_update_fails(hass, aioclient_mock):
"""Test setup fails if first update fails."""
aioclient_mock.get(duckdns.UPDATE_URL, params={
'domains': DOMAIN,
'token': TOKEN
}, text='KO')
result = yield from async_setup_component(hass, duckdns.DOMAIN, {
'duckdns': {
'domain': DOMAIN,
'access_token': TOKEN
}
})
assert not result
assert aioclient_mock.call_count == 1
@asyncio.coroutine
def test_service_set_txt(hass, aioclient_mock, setup_duckdns):
"""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',
}, text='OK')
assert aioclient_mock.call_count == 0
yield from hass.components.duckdns.async_set_txt('some-txt')
assert aioclient_mock.call_count == 1
@asyncio.coroutine
def test_service_clear_txt(hass, aioclient_mock, setup_duckdns):
"""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',
}, text='OK')
assert aioclient_mock.call_count == 0
yield from hass.components.duckdns.async_set_txt(None)
assert aioclient_mock.call_count == 1