8000 Add DuckDNS component by balloob · Pull Request #9556 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add DuckDNS component #9556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions homeassistant/components/duckdns.py
8000
Original file line number Diff line number Diff line change
@@ -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
106 changes: 106 additions & 0 deletions
Original file line number Diff line number Diff line change
@@ -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
0