[new] component rest_command (#5055)
* New component rest_command * add unittests * change handling like other command * change unittest * address @balloob comments
This commit is contained in:
parent
1719d88602
commit
2b991e2f32
2 changed files with 338 additions and 0 deletions
115
homeassistant/components/rest_command.py
Normal file
115
homeassistant/components/rest_command.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
"""
|
||||||
|
Exposes regular rest commands as services.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/rest_command/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_TIMEOUT, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_PAYLOAD,
|
||||||
|
CONF_METHOD)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
DOMAIN = 'rest_command'
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
DEFAULT_METHOD = 'get'
|
||||||
|
|
||||||
|
SUPPORT_REST_METHODS = [
|
||||||
|
'get',
|
||||||
|
'post',
|
||||||
|
'put',
|
||||||
|
'delete',
|
||||||
|
]
|
||||||
|
|
||||||
|
COMMAND_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_URL): cv.template,
|
||||||
|
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD):
|
||||||
|
vol.All(vol.Lower, vol.In(SUPPORT_REST_METHODS)),
|
||||||
|
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||||
|
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||||
|
vol.Optional(CONF_PAYLOAD): cv.template,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
cv.slug: COMMAND_SCHEMA,
|
||||||
|
}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
|
"""Setup the rest_command component."""
|
||||||
|
websession = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
def async_register_rest_command(name, command_config):
|
||||||
|
"""Create service for rest command."""
|
||||||
|
timeout = command_config[CONF_TIMEOUT]
|
||||||
|
method = command_config[CONF_METHOD]
|
||||||
|
|
||||||
|
template_url = command_config[CONF_URL]
|
||||||
|
template_url.hass = hass
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if CONF_USERNAME in command_config:
|
||||||
|
username = command_config[CONF_USERNAME]
|
||||||
|
password = command_config.get(CONF_PASSWORD, '')
|
||||||
|
auth = aiohttp.BasicAuth(username, password=password)
|
||||||
|
|
||||||
|
template_payload = None
|
||||||
|
if CONF_PAYLOAD in command_config:
|
||||||
|
template_payload = command_config[CONF_PAYLOAD]
|
||||||
|
template_payload.hass = hass
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_service_handler(service):
|
||||||
|
"""Execute a shell command service."""
|
||||||
|
payload = None
|
||||||
|
if template_payload:
|
||||||
|
payload = bytes(
|
||||||
|
template_payload.async_render(variables=service.data),
|
||||||
|
'utf-8')
|
||||||
|
|
||||||
|
request = None
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
|
request = yield from getattr(websession, method)(
|
||||||
|
template_url.async_render(variables=service.data),
|
||||||
|
data=payload,
|
||||||
|
auth=auth
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.status == 200:
|
||||||
|
_LOGGER.info("Success call %s.", request.url)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Error %d on call %s.", request.status, request.url)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Timeout call %s.", request.url)
|
||||||
|
|
||||||
|
except aiohttp.errors.ClientError:
|
||||||
|
_LOGGER.error("Client error %s.", request.url)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if request is not None:
|
||||||
|
yield from request.release()
|
||||||
|
|
||||||
|
# register services
|
||||||
|
hass.services.async_register(DOMAIN, name, async_service_handler)
|
||||||
|
|
||||||
|
for command, command_config in config[DOMAIN].items():
|
||||||
|
async_register_rest_command(command, command_config)
|
||||||
|
|
||||||
|
return True
|
223
tests/components/test_rest_command.py
Normal file
223
tests/components/test_rest_command.py
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
"""The tests for the rest command platform."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
import homeassistant.components.rest_command as rc
|
||||||
|
from homeassistant.bootstrap import setup_component
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
get_test_home_assistant, assert_setup_component)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRestCommandSetup(object):
|
||||||
|
"""Test the rest command component."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Setup things to be run when tests are started."""
|
||||||
|
self.hass = get_test_home_assistant()
|
||||||
|
|
||||||
|
self.config = {
|
||||||
|
rc.DOMAIN: {'test_get': {
|
||||||
|
'url': 'http://example.com/'
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
"""Stop everything that was started."""
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_setup_component(self):
|
||||||
|
"""Test setup component."""
|
||||||
|
with assert_setup_component(1):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
def test_setup_component_timeout(self):
|
||||||
|
"""Test setup component timeout."""
|
||||||
|
self.config[rc.DOMAIN]['test_get']['timeout'] = 10
|
||||||
|
|
||||||
|
with assert_setup_component(1):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
def test_setup_component_test_service(self):
|
||||||
|
"""Test setup component and check if service exits."""
|
||||||
|
with assert_setup_component(1):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
assert self.hass.services.has_service(rc.DOMAIN, 'test_get')
|
||||||
|
|
||||||
|
|
||||||
|
class TestRestCommandComponent(object):
|
||||||
|
"""Test the rest command component."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Setup things to be run when tests are started."""
|
||||||
|
self.url = "https://example.com/"
|
||||||
|
self.config = {
|
||||||
|
rc.DOMAIN: {
|
||||||
|
'get_test': {
|
||||||
|
'url': self.url,
|
||||||
|
'method': 'get',
|
||||||
|
},
|
||||||
|
'post_test': {
|
||||||
|
'url': self.url,
|
||||||
|
'method': 'post',
|
||||||
|
},
|
||||||
|
'put_test': {
|
||||||
|
'url': self.url,
|
||||||
|
'method': 'put',
|
||||||
|
},
|
||||||
|
'delete_test': {
|
||||||
|
'url': self.url,
|
||||||
|
'method': 'delete',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hass = get_test_home_assistant()
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
"""Stop everything that was started."""
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_setup_tests(self):
|
||||||
|
"""Setup test config and test it."""
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
assert self.hass.services.has_service(rc.DOMAIN, 'get_test')
|
||||||
|
assert self.hass.services.has_service(rc.DOMAIN, 'post_test')
|
||||||
|
assert self.hass.services.has_service(rc.DOMAIN, 'put_test')
|
||||||
|
assert self.hass.services.has_service(rc.DOMAIN, 'delete_test')
|
||||||
|
|
||||||
|
def test_rest_command_timeout(self, aioclient_mock):
|
||||||
|
"""Call a rest command with timeout."""
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.get(self.url, exc=asyncio.TimeoutError())
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'get_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
def test_rest_command_aiohttp_error(self, aioclient_mock):
|
||||||
|
"""Call a rest command with aiohttp exception."""
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.get(self.url, exc=aiohttp.errors.ClientError())
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'get_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
def test_rest_command_http_error(self, aioclient_mock):
|
||||||
|
"""Call a rest command with status code 400."""
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.get(self.url, status=400)
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'get_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
def test_rest_command_auth(self, aioclient_mock):
|
||||||
|
"""Call a rest command with auth credential."""
|
||||||
|
data = {
|
||||||
|
'username': 'test',
|
||||||
|
'password': '123456',
|
||||||
|
}
|
||||||
|
self.config[rc.DOMAIN]['get_test'].update(data)
|
||||||
|
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.get(self.url, content=b'success')
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'get_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
def test_rest_command_form_data(self, aioclient_mock):
|
||||||
|
"""Call a rest command with post form data."""
|
||||||
|
data = {
|
||||||
|
'payload': 'test'
|
||||||
|
}
|
||||||
|
self.config[rc.DOMAIN]['post_test'].update(data)
|
||||||
|
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.post(self.url, content=b'success')
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'post_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
assert aioclient_mock.mock_calls[0][2] == b'test'
|
||||||
|
|
||||||
|
def test_rest_command_get(self, aioclient_mock):
|
||||||
|
"""Call a rest command with get."""
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.get(self.url, content=b'success')
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'get_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
def test_rest_command_delete(self, aioclient_mock):
|
||||||
|
"""Call a rest command with delete."""
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.delete(self.url, content=b'success')
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'delete_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
def test_rest_command_post(self, aioclient_mock):
|
||||||
|
"""Call a rest command with post."""
|
||||||
|
data = {
|
||||||
|
'payload': 'data',
|
||||||
|
}
|
||||||
|
self.config[rc.DOMAIN]['post_test'].update(data)
|
||||||
|
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.post(self.url, content=b'success')
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'post_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
assert aioclient_mock.mock_calls[0][2] == b'data'
|
||||||
|
|
||||||
|
def test_rest_command_put(self, aioclient_mock):
|
||||||
|
"""Call a rest command with put."""
|
||||||
|
data = {
|
||||||
|
'payload': 'data',
|
||||||
|
}
|
||||||
|
self.config[rc.DOMAIN]['put_test'].update(data)
|
||||||
|
|
||||||
|
with assert_setup_component(4):
|
||||||
|
setup_component(self.hass, rc.DOMAIN, self.config)
|
||||||
|
|
||||||
|
aioclient_mock.put(self.url, content=b'success')
|
||||||
|
|
||||||
|
self.hass.services.call(rc.DOMAIN, 'put_test', {})
|
||||||
|
self.hass.block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
assert aioclient_mock.mock_calls[0][2] == b'data'
|
Loading…
Add table
Add a link
Reference in a new issue