Hassio api v3 (#7323)

* HassIO rest API v3

* fix content type

* fix lint

* Update comment

* fix content type

* change proxy handling

* fix handling

* fix register

* fix addons

* fix routing

* Update hassio to just proxy

* Fix tests

* Lint
This commit is contained in:
Paulus Schoutsen 2017-04-26 22:36:48 -07:00 committed by GitHub
parent 3374169c74
commit b14c07a60c
2 changed files with 261 additions and 807 deletions

View File

@ -7,70 +7,52 @@ https://home-assistant.io/components/hassio/
import asyncio import asyncio
import logging import logging
import os import os
import re
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway from aiohttp.web_exceptions import (
HTTPBadGateway, HTTPNotFound, HTTPMethodNotAllowed)
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout import async_timeout
import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
DOMAIN = 'hassio' DOMAIN = 'hassio'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
LONG_TASK_TIMEOUT = 900 TIMEOUT = 10
DEFAULT_TIMEOUT = 10
SERVICE_HOST_SHUTDOWN = 'host_shutdown' HASSIO_REST_COMMANDS = {
SERVICE_HOST_REBOOT = 'host_reboot' 'host/shutdown': ['POST'],
'host/reboot': ['POST'],
'host/update': ['GET'],
'host/info': ['GET'],
'supervisor/info': ['GET'],
'supervisor/update': ['POST'],
'supervisor/options': ['POST'],
'supervisor/reload': ['POST'],
'supervisor/logs': ['GET'],
'homeassistant/info': ['GET'],
'homeassistant/update': ['POST'],
'homeassistant/logs': ['GET'],
'network/info': ['GET'],
'network/options': ['GET'],
}
SERVICE_HOST_UPDATE = 'host_update' ADDON_REST_COMMANDS = {
SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update' 'install': ['POST'],
'uninstall': ['POST'],
SERVICE_SUPERVISOR_UPDATE = 'supervisor_update' 'start': ['POST'],
SERVICE_SUPERVISOR_RELOAD = 'supervisor_reload' 'stop': ['POST'],
'update': ['POST'],
SERVICE_ADDON_INSTALL = 'addon_install' 'options': ['POST'],
SERVICE_ADDON_UNINSTALL = 'addon_uninstall' 'info': ['GET'],
SERVICE_ADDON_UPDATE = 'addon_update' 'logs': ['GET'],
SERVICE_ADDON_START = 'addon_start'
SERVICE_ADDON_STOP = 'addon_stop'
ATTR_ADDON = 'addon'
ATTR_VERSION = 'version'
SCHEMA_SERVICE_UPDATE = vol.Schema({
vol.Optional(ATTR_VERSION): cv.string,
})
SCHEMA_SERVICE_ADDONS = vol.Schema({
vol.Required(ATTR_ADDON): cv.slug,
})
SCHEMA_SERVICE_ADDONS_VERSION = SCHEMA_SERVICE_ADDONS.extend({
vol.Optional(ATTR_VERSION): cv.string,
})
SERVICE_MAP = {
SERVICE_HOST_SHUTDOWN: None,
SERVICE_HOST_REBOOT: None,
SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_SUPERVISOR_RELOAD: None,
SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION,
SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_STOP: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_UPDATE: SCHEMA_SERVICE_ADDONS_VERSION,
} }
@ -91,71 +73,7 @@ def async_setup(hass, config):
_LOGGER.error("Not connected with HassIO!") _LOGGER.error("Not connected with HassIO!")
return False return False
# register base api views hass.http.register_view(HassIOView(hassio))
for base in ('host', 'homeassistant'):
hass.http.register_view(HassIOBaseView(hassio, base))
for base in ('supervisor', 'network'):
hass.http.register_view(HassIOBaseEditView(hassio, base))
for base in ('supervisor', 'homeassistant'):
hass.http.register_view(HassIOBaseLogsView(hassio, base))
# register view for addons
hass.http.register_view(HassIOAddonsView(hassio))
hass.http.register_view(HassIOAddonsLogsView(hassio))
@asyncio.coroutine
def async_service_handler(service):
"""Handle HassIO service calls."""
addon = service.data.get(ATTR_ADDON)
if ATTR_VERSION in service.data:
version = {ATTR_VERSION: service.data[ATTR_VERSION]}
else:
version = None
# map to api call
if service.service == SERVICE_HOST_UPDATE:
yield from hassio.send_command(
"/host/update", payload=version)
elif service.service == SERVICE_HOST_REBOOT:
yield from hassio.send_command("/host/reboot")
elif service.service == SERVICE_HOST_SHUTDOWN:
yield from hassio.send_command("/host/shutdown")
elif service.service == SERVICE_SUPERVISOR_UPDATE:
yield from hassio.send_command(
"/supervisor/update", payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_SUPERVISOR_RELOAD:
yield from hassio.send_command(
"/supervisor/reload", timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_HOMEASSISTANT_UPDATE:
yield from hassio.send_command(
"/homeassistant/update", payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_INSTALL:
yield from hassio.send_command(
"/addons/{}/install".format(addon), payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_UNINSTALL:
yield from hassio.send_command(
"/addons/{}/uninstall".format(addon))
elif service.service == SERVICE_ADDON_START:
yield from hassio.send_command("/addons/{}/start".format(addon))
elif service.service == SERVICE_ADDON_STOP:
yield from hassio.send_command(
"/addons/{}/stop".format(addon), timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_UPDATE:
yield from hassio.send_command(
"/addons/{}/update".format(addon), payload=version,
timeout=LONG_TASK_TIMEOUT)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service, schema in SERVICE_MAP.items():
hass.services.async_register(
DOMAIN, service, async_service_handler,
descriptions[DOMAIN][service], schema=schema)
return True return True
@ -169,165 +87,122 @@ class HassIO(object):
self.websession = websession self.websession = websession
self._ip = ip self._ip = ip
@asyncio.coroutine
def is_connected(self): def is_connected(self):
"""Return True if it connected to HassIO supervisor. """Return True if it connected to HassIO supervisor.
Return a coroutine. This method is a coroutine.
""" """
return self.send_command("/supervisor/ping") try:
with async_timeout.timeout(TIMEOUT, loop=self.loop):
request = yield from self.websession.get(
"http://{}{}".format(self._ip, "/supervisor/ping")
)
@asyncio.coroutine if request.status != 200:
def send_command(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT): _LOGGER.error("Ping return code %d.", request.status)
"""Send request to API.""" return False
answer = yield from self.send_raw(
cmd, payload=payload, timeout=timeout answer = yield from request.json()
) return answer and answer['result'] == 'ok'
if answer and answer['result'] == 'ok':
return answer['data'] if answer['data'] else True except asyncio.TimeoutError:
elif answer: _LOGGER.error("Timeout on ping request")
_LOGGER.error("%s return error %s.", cmd, answer['message'])
except aiohttp.ClientError as err:
_LOGGER.error("Client error on ping request %s", err)
return False return False
@asyncio.coroutine @asyncio.coroutine
def send_raw(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT, json=True): def command_proxy(self, path, request):
"""Send raw request to API.""" """Return a client request with proxy origin for HassIO supervisor.
This method is a coroutine.
"""
try: try:
with async_timeout.timeout(timeout, loop=self.loop): data = None
request = yield from self.websession.get( headers = None
"http://{}{}".format(self._ip, cmd), with async_timeout.timeout(TIMEOUT, loop=self.loop):
timeout=None, json=payload data = yield from request.read()
) if data:
headers = {CONTENT_TYPE: request.content_type}
else:
data = None
if request.status != 200: method = getattr(self.websession, request.method.lower())
_LOGGER.error("%s return code %d.", cmd, request.status) client = yield from method(
return "http://{}/{}".format(self._ip, path), data=data,
headers=headers
)
if json: return client
return (yield from request.json())
# get raw output except aiohttp.ClientError as err:
return (yield from request.read()) _LOGGER.error("Client error on api %s request %s.", path, err)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error("Timeout on api request %s.", cmd) _LOGGER.error("Client timeout error on api request %s.", path)
except aiohttp.ClientError: raise HTTPBadGateway()
_LOGGER.error("Client error on api request %s.", cmd)
class HassIOBaseView(HomeAssistantView): class HassIOView(HomeAssistantView):
"""HassIO view to handle base part.""" """HassIO view to handle base part."""
name = "api:hassio"
url = "/api/hassio/{path:.+}"
requires_auth = True requires_auth = True
def __init__(self, hassio, base):
"""Initialize a hassio base view."""
self.hassio = hassio
self._url_info = "/{}/info".format(base)
self.url = "/api/hassio/{}".format(base)
self.name = "api:hassio:{}".format(base)
@asyncio.coroutine
def get(self, request):
"""Get base data."""
data = yield from self.hassio.send_command(self._url_info)
if not data:
raise HTTPBadGateway()
return web.json_response(data)
class HassIOBaseEditView(HassIOBaseView):
"""HassIO view to handle base with options support."""
def __init__(self, hassio, base):
"""Initialize a hassio base edit view."""
super().__init__(hassio, base)
self._url_options = "/{}/options".format(base)
@asyncio.coroutine
def post(self, request):
"""Set options on host."""
data = yield from request.json()
response = yield from self.hassio.send_raw(
self._url_options, payload=data)
if not response:
raise HTTPBadGateway()
return web.json_response(response)
class HassIOBaseLogsView(HomeAssistantView):
"""HassIO view to handle base logs part."""
requires_auth = True
def __init__(self, hassio, base):
"""Initialize a hassio base view."""
self.hassio = hassio
self._url_logs = "/{}/logs".format(base)
self.url = "/api/hassio/logs/{}".format(base)
self.name = "api:hassio:logs:{}".format(base)
@asyncio.coroutine
def get(self, request):
"""Get logs."""
data = yield from self.hassio.send_raw(self._url_logs, json=False)
if not data:
raise HTTPBadGateway()
return web.Response(body=data)
class HassIOAddonsView(HomeAssistantView):
"""HassIO view to handle addons part."""
requires_auth = True
url = "/api/hassio/addons/{addon}"
name = "api:hassio:addons"
def __init__(self, hassio): def __init__(self, hassio):
"""Initialize a hassio addon view.""" """Initialize a hassio base view."""
self.hassio = hassio self.hassio = hassio
@asyncio.coroutine @asyncio.coroutine
def get(self, request, addon): def _handle(self, request, path):
"""Get addon data.""" """Route data to hassio."""
data = yield from self.hassio.send_command( if path.startswith('addons/'):
"/addons/{}/info".format(addon)) parts = path.split('/')
if not data:
raise HTTPBadGateway()
return web.json_response(data)
@asyncio.coroutine if len(parts) != 3:
def post(self, request, addon): raise HTTPNotFound()
"""Set options on host."""
data = yield from request.json()
response = yield from self.hassio.send_raw( allowed_methods = ADDON_REST_COMMANDS.get(parts[-1])
"/addons/{}/options".format(addon), payload=data) else:
if not response: allowed_methods = HASSIO_REST_COMMANDS.get(path)
raise HTTPBadGateway()
return web.json_response(response) if allowed_methods is None:
raise HTTPNotFound()
if request.method not in allowed_methods:
raise HTTPMethodNotAllowed(request.method, allowed_methods)
client = yield from self.hassio.command_proxy(path, request)
data = yield from client.read()
if path.endswith('/logs'):
return _create_response_log(client, data)
return _create_response(client, data)
get = _handle
post = _handle
class HassIOAddonsLogsView(HomeAssistantView): def _create_response(client, data):
"""HassIO view to handle addons logs part.""" """Convert a response from client request."""
return web.Response(
body=data,
status=client.status,
content_type=client.content_type,
)
requires_auth = True
url = "/api/hassio/logs/addons/{addon}"
name = "api:hassio:logs:addons"
def __init__(self, hassio): def _create_response_log(client, data):
"""Initialize a hassio addon view.""" """Convert a response from client request."""
self.hassio = hassio # Remove color codes
log = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", data.decode())
@asyncio.coroutine return web.Response(
def get(self, request, addon): text=log,
"""Get addon data.""" status=client.status,
data = yield from self.hassio.send_raw( content_type=CONTENT_TYPE_TEXT_PLAIN,
"/addons/{}/logs".format(addon), json=False) )
if not data:
raise HTTPBadGateway()
return web.Response(body=data)

View File

@ -1,605 +1,184 @@
"""The tests for the hassio component.""" """The tests for the hassio component."""
import asyncio import asyncio
import os import os
from unittest.mock import patch from unittest.mock import patch, Mock, MagicMock
import aiohttp
import pytest import pytest
import homeassistant.components.hassio as ho import homeassistant.components.hassio as ho
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import async_setup_component
from tests.common import ( from tests.common import mock_coro, mock_http_component_app
get_test_home_assistant, assert_setup_component)
@pytest.fixture @pytest.fixture
def hassio_env(): def hassio_env():
"""Fixture to inject hassio env.""" """Fixture to inject hassio env."""
with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}) as env_mock: with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \
yield env_mock patch('homeassistant.components.hassio.HassIO.is_connected',
Mock(return_value=mock_coro(True))):
yield
class TestHassIOSetup(object):
"""Test the hassio component."""
@pytest.fixture
def setup_method(self): def hassio_client(hassio_env, hass, test_client):
"""Setup things to be run when tests are started.""" """Create mock hassio http client."""
self.hass = get_test_home_assistant() app = mock_http_component_app(hass)
hass.loop.run_until_complete(async_setup_component(hass, 'hassio', {}))
self.config = { hass.http.views['api:hassio'].register(app.router)
ho.DOMAIN: {}, yield hass.loop.run_until_complete(test_client(app))
}
def teardown_method(self):
"""Stop everything that was started."""
self.hass.stop()
def test_setup_component(self, aioclient_mock, hassio_env):
"""Test setup component."""
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={
'result': 'ok', 'data': {}
})
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
def test_setup_component_bad(self, aioclient_mock):
"""Test setup component bad."""
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={
'result': 'ok', 'data': {}
})
with assert_setup_component(0, ho.DOMAIN):
assert not setup_component(self.hass, ho.DOMAIN, self.config)
assert len(aioclient_mock.mock_calls) == 0
def test_setup_component_test_service(self, aioclient_mock, hassio_env):
"""Test setup component and check if service exits."""
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={
'result': 'ok', 'data': {}
})
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_HOST_REBOOT)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_HOST_SHUTDOWN)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_HOST_UPDATE)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_SUPERVISOR_UPDATE)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_SUPERVISOR_RELOAD)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_ADDON_INSTALL)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_ADDON_UNINSTALL)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_ADDON_UPDATE)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_ADDON_START)
assert self.hass.services.has_service(
ho.DOMAIN, ho.SERVICE_ADDON_STOP)
class TestHassIOComponent(object):
"""Test the HassIO component."""
def setup_method(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.config = {
ho.DOMAIN: {},
}
self.url = "http://127.0.0.1/{}"
self.error_msg = {
'result': 'error',
'message': 'Test error',
}
self.ok_msg = {
'result': 'ok',
'data': {},
}
def teardown_method(self):
"""Stop everything that was started."""
self.hass.stop()
def test_rest_command_timeout(self, aioclient_mock, hassio_env):
"""Call a hassio with timeout."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/update"), exc=asyncio.TimeoutError())
self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_aiohttp_error(self, aioclient_mock, hassio_env):
"""Call a hassio with aiohttp exception."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/update"), exc=aiohttp.ClientError())
self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_error(self, aioclient_mock, hassio_env):
"""Call a hassio with status code 503."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/update"), status=503)
self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_error_api(self, aioclient_mock, hassio_env):
"""Call a hassio with status code 503."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/update"), json=self.error_msg)
self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_host_reboot(self, aioclient_mock, hassio_env):
"""Call a hassio for host reboot."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/reboot"), json=self.ok_msg)
self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_REBOOT, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_host_shutdown(self, aioclient_mock, hassio_env):
"""Call a hassio for host shutdown."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/shutdown"), json=self.ok_msg)
self.hass.services.call(ho.DOMAIN, ho.SERVICE_HOST_SHUTDOWN, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_host_update(self, aioclient_mock, hassio_env):
"""Call a hassio for host update."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("host/update"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_HOST_UPDATE, {'version': '0.4'})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4'
def test_rest_command_http_supervisor_update(self, aioclient_mock,
hassio_env):
"""Call a hassio for supervisor update."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("supervisor/update"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_SUPERVISOR_UPDATE, {'version': '0.4'})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4'
def test_rest_command_http_supervisor_reload(self, aioclient_mock,
hassio_env):
"""Call a hassio for supervisor reload."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("supervisor/reload"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_SUPERVISOR_RELOAD, {})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_homeassistant_update(self, aioclient_mock,
hassio_env):
"""Call a hassio for homeassistant update."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("homeassistant/update"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_HOMEASSISTANT_UPDATE, {'version': '0.4'})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4'
def test_rest_command_http_addon_install(self, aioclient_mock, hassio_env):
"""Call a hassio for addon install."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("addons/smb_config/install"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_ADDON_INSTALL, {
'addon': 'smb_config',
'version': '0.4'
})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4'
def test_rest_command_http_addon_uninstall(self, aioclient_mock,
hassio_env):
"""Call a hassio for addon uninstall."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("addons/smb_config/uninstall"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_ADDON_UNINSTALL, {
'addon': 'smb_config'
})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_addon_update(self, aioclient_mock, hassio_env):
"""Call a hassio for addon update."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("addons/smb_config/update"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_ADDON_UPDATE, {
'addon': 'smb_config',
'version': '0.4'
})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4'
def test_rest_command_http_addon_start(self, aioclient_mock, hassio_env):
"""Call a hassio for addon start."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("addons/smb_config/start"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_ADDON_START, {
'addon': 'smb_config',
})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
def test_rest_command_http_addon_stop(self, aioclient_mock, hassio_env):
"""Call a hassio for addon stop."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/ping", json=self.ok_msg)
with assert_setup_component(0, ho.DOMAIN):
setup_component(self.hass, ho.DOMAIN, self.config)
aioclient_mock.get(
self.url.format("addons/smb_config/stop"), json=self.ok_msg)
self.hass.services.call(
ho.DOMAIN, ho.SERVICE_ADDON_STOP, {
'addon': 'smb_config'
})
self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 2
@asyncio.coroutine @asyncio.coroutine
def test_async_hassio_host_view(aioclient_mock, hass, test_client, hassio_env): def test_fail_setup_without_environ_var(hass):
"""Test that it fetches the given url.""" """Fail setup if no environ variable set."""
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ with patch.dict(os.environ, {}, clear=True):
'result': 'ok', 'data': {} result = yield from async_setup_component(hass, 'hassio', {})
}) assert not result
result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}})
assert result, 'Failed to setup hasio'
client = yield from test_client(hass.http.app)
aioclient_mock.get('http://127.0.0.1/host/info', json={
'result': 'ok',
'data': {
'os': 'resinos',
'version': '0.3',
'current': '0.4',
'level': 16,
'hostname': 'test',
}
})
resp = yield from client.get('/api/hassio/host')
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 2
assert resp.status == 200
assert data['os'] == 'resinos'
assert data['version'] == '0.3'
assert data['current'] == '0.4'
assert data['level'] == 16
assert data['hostname'] == 'test'
@asyncio.coroutine @asyncio.coroutine
def test_async_hassio_homeassistant_view(aioclient_mock, hass, test_client, def test_fail_setup_cannot_connect(hass):
hassio_env): """Fail setup if cannot connect."""
"""Test that it fetches the given url.""" with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ patch('homeassistant.components.hassio.HassIO.is_connected',
'result': 'ok', 'data': {} Mock(return_value=mock_coro(False))):
}) result = yield from async_setup_component(hass, 'hassio', {})
result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}}) assert not result
assert result, 'Failed to setup hasio'
client = yield from test_client(hass.http.app)
aioclient_mock.get('http://127.0.0.1/homeassistant/info', json={
'result': 'ok',
'data': {
'version': '0.41',
'current': '0.41.1',
}
})
resp = yield from client.get('/api/hassio/homeassistant')
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 2
assert resp.status == 200
assert data['version'] == '0.41'
assert data['current'] == '0.41.1'
aioclient_mock.get('http://127.0.0.1/homeassistant/logs',
content=b"That is a test log")
resp = yield from client.get('/api/hassio/logs/homeassistant')
data = yield from resp.read()
assert len(aioclient_mock.mock_calls) == 3
assert resp.status == 200
assert data == b"That is a test log"
@asyncio.coroutine @asyncio.coroutine
def test_async_hassio_supervisor_view(aioclient_mock, hass, test_client, def test_invalid_path(hassio_client):
hassio_env): """Test requesting invalid path."""
"""Test that it fetches the given url.""" with patch.dict(ho.HASSIO_REST_COMMANDS, {}, clear=True):
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ resp = yield from hassio_client.post('/api/hassio/beer')
'result': 'ok', 'data': {}
})
result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}})
assert result, 'Failed to setup hasio'
client = yield from test_client(hass.http.app) assert resp.status == 404
aioclient_mock.get('http://127.0.0.1/supervisor/info', json={
'result': 'ok',
'data': {
'version': '0.3',
'current': '0.4',
'beta': False,
}
})
resp = yield from client.get('/api/hassio/supervisor')
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 2
assert resp.status == 200
assert data['version'] == '0.3'
assert data['current'] == '0.4'
assert not data['beta']
aioclient_mock.get('http://127.0.0.1/supervisor/options', json={
'result': 'ok',
'data': {},
})
resp = yield from client.post('/api/hassio/supervisor', json={
'beta': True,
})
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 3
assert resp.status == 200
assert aioclient_mock.mock_calls[-1][2]['beta']
aioclient_mock.get('http://127.0.0.1/supervisor/logs',
content=b"That is a test log")
resp = yield from client.get('/api/hassio/logs/supervisor')
data = yield from resp.read()
assert len(aioclient_mock.mock_calls) == 4
assert resp.status == 200
assert data == b"That is a test log"
@asyncio.coroutine @asyncio.coroutine
def test_async_hassio_network_view(aioclient_mock, hass, test_client, def test_invalid_method(hassio_client):
hassio_env): """Test requesting path with invalid method."""
"""Test that it fetches the given url.""" with patch.dict(ho.HASSIO_REST_COMMANDS, {'beer': ['POST']}):
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ resp = yield from hassio_client.get('/api/hassio/beer')
'result': 'ok', 'data': {}
})
result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}})
assert result, 'Failed to setup hasio'
client = yield from test_client(hass.http.app) assert resp.status == 405
aioclient_mock.get('http://127.0.0.1/network/info', json={
'result': 'ok',
'data': {
'mode': 'dhcp',
'ssid': 'my_wlan',
'password': '123456',
}
})
resp = yield from client.get('/api/hassio/network')
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 2
assert resp.status == 200
assert data['mode'] == 'dhcp'
assert data['ssid'] == 'my_wlan'
assert data['password'] == '123456'
aioclient_mock.get('http://127.0.0.1/network/options', json={
'result': 'ok',
'data': {},
})
resp = yield from client.post('/api/hassio/network', json={
'mode': 'dhcp',
'ssid': 'my_wlan2',
'password': '654321',
})
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 3
assert resp.status == 200
assert aioclient_mock.mock_calls[-1][2]['ssid'] == 'my_wlan2'
assert aioclient_mock.mock_calls[-1][2]['password'] == '654321'
@asyncio.coroutine @asyncio.coroutine
def test_async_hassio_addon_view(aioclient_mock, hass, test_client, def test_forward_normal_path(hassio_client):
hassio_env): """Test fetching normal path."""
"""Test that it fetches the given url.""" response = MagicMock()
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={ response.read.return_value = mock_coro('data')
'result': 'ok', 'data': {}
})
result = yield from async_setup_component(hass, ho.DOMAIN, {ho.DOMAIN: {}})
assert result, 'Failed to setup hasio'
client = yield from test_client(hass.http.app) with patch.dict(ho.HASSIO_REST_COMMANDS, {'beer': ['POST']}), \
patch('homeassistant.components.hassio.HassIO.command_proxy',
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio._create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.post('/api/hassio/beer')
aioclient_mock.get('http://127.0.0.1/addons/smb_config/info', json={ # Check we got right response
'result': 'ok',
'data': {
'name': 'SMB Config',
'state': 'running',
'boot': 'auto',
'options': {
'bla': False,
}
}
})
resp = yield from client.get('/api/hassio/addons/smb_config')
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 2
assert resp.status == 200 assert resp.status == 200
assert data['name'] == 'SMB Config' body = yield from resp.text()
assert data['state'] == 'running' assert body == 'response'
assert data['boot'] == 'auto'
assert not data['options']['bla']
aioclient_mock.get('http://127.0.0.1/addons/smb_config/options', json={ # Check we forwarded command
'result': 'ok', assert len(mresp.mock_calls) == 1
'data': {}, assert mresp.mock_calls[0][1] == (response, 'data')
})
resp = yield from client.post('/api/hassio/addons/smb_config', json={
'boot': 'manual',
'options': {
'bla': True,
}
})
data = yield from resp.json()
assert len(aioclient_mock.mock_calls) == 3 @asyncio.coroutine
def test_forward_normal_log_path(hassio_client):
"""Test fetching normal log path."""
response = MagicMock()
response.read.return_value = mock_coro('data')
with patch.dict(ho.HASSIO_REST_COMMANDS, {'beer/logs': ['GET']}), \
patch('homeassistant.components.hassio.HassIO.command_proxy',
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.'
'_create_response_log') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.get('/api/hassio/beer/logs')
# Check we got right response
assert resp.status == 200 assert resp.status == 200
assert aioclient_mock.mock_calls[-1][2]['boot'] == 'manual' body = yield from resp.text()
assert aioclient_mock.mock_calls[-1][2]['options']['bla'] assert body == 'response'
aioclient_mock.get('http://127.0.0.1/addons/smb_config/logs', # Check we forwarded command
content=b"That is a test log") assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
resp = yield from client.get('/api/hassio/logs/addons/smb_config')
data = yield from resp.read()
assert len(aioclient_mock.mock_calls) == 4 @asyncio.coroutine
def test_forward_addon_path(hassio_client):
"""Test fetching addon path."""
response = MagicMock()
response.read.return_value = mock_coro('data')
with patch.dict(ho.ADDON_REST_COMMANDS, {'install': ['POST']}), \
patch('homeassistant.components.hassio.'
'HassIO.command_proxy') as proxy_command, \
patch('homeassistant.components.hassio._create_response') as mresp:
proxy_command.return_value = mock_coro(response)
mresp.return_value = 'response'
resp = yield from hassio_client.post('/api/hassio/addons/beer/install')
# Check we got right response
assert resp.status == 200 assert resp.status == 200
assert data == b"That is a test log" body = yield from resp.text()
assert body == 'response'
assert proxy_command.mock_calls[0][1][0] == 'addons/beer/install'
# Check we forwarded command
assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
@asyncio.coroutine
def test_forward_addon_log_path(hassio_client):
"""Test fetching addon log path."""
response = MagicMock()
response.read.return_value = mock_coro('data')
with patch.dict(ho.ADDON_REST_COMMANDS, {'logs': ['GET']}), \
patch('homeassistant.components.hassio.'
'HassIO.command_proxy') as proxy_command, \
patch('homeassistant.components.hassio.'
'_create_response_log') as mresp:
proxy_command.return_value = mock_coro(response)
mresp.return_value = 'response'
resp = yield from hassio_client.get('/api/hassio/addons/beer/logs')
# Check we got right response
assert resp.status == 200
body = yield from resp.text()
assert body == 'response'
assert proxy_command.mock_calls[0][1][0] == 'addons/beer/logs'
# Check we forwarded command
assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
@asyncio.coroutine
def test_bad_request_when_wrong_addon_url(hassio_client):
"""Test we cannot mess with addon url."""
resp = yield from hassio_client.get('/api/hassio/addons/../../info')
assert resp.status == 404
resp = yield from hassio_client.get('/api/hassio/addons/info')
assert resp.status == 404
@asyncio.coroutine
def test_bad_gateway_when_cannot_find_supervisor(hassio_client):
"""Test we get a bad gateway error if we can't find supervisor."""
with patch('homeassistant.components.hassio.async_timeout.timeout',
side_effect=asyncio.TimeoutError):
resp = yield from hassio_client.get('/api/hassio/addons/test/info')
assert resp.status == 502