From 2b991e2f3221cf69aeef951c635f43c25a1fe859 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 6 Jan 2017 23:42:53 +0100 Subject: [PATCH] [new] component rest_command (#5055) * New component rest_command * add unittests * change handling like other command * change unittest * address @balloob comments --- homeassistant/components/rest_command.py | 115 ++++++++++++ tests/components/test_rest_command.py | 223 +++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 homeassistant/components/rest_command.py create mode 100644 tests/components/test_rest_command.py diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py new file mode 100644 index 00000000000..dfcb5610073 --- /dev/null +++ b/homeassistant/components/rest_command.py @@ -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 diff --git a/tests/components/test_rest_command.py b/tests/components/test_rest_command.py new file mode 100644 index 00000000000..8fe9523801d --- /dev/null +++ b/tests/components/test_rest_command.py @@ -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'