mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 22:37:11 +00:00
Support new feature to push API data to hassio (#9679)
* Support new featuer to push API data to hassio * Add tests & services
This commit is contained in:
parent
a4b64dec39
commit
4be91a103d
@ -14,9 +14,13 @@ from aiohttp import web
|
|||||||
from aiohttp.web_exceptions import HTTPBadGateway
|
from aiohttp.web_exceptions import HTTPBadGateway
|
||||||
from aiohttp.hdrs import CONTENT_TYPE
|
from aiohttp.hdrs import CONTENT_TYPE
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
|
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
|
||||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
from homeassistant.components.http import (
|
||||||
|
HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT,
|
||||||
|
CONF_SSL_CERTIFICATE)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.components.frontend import register_built_in_panel
|
from homeassistant.components.frontend import register_built_in_panel
|
||||||
|
|
||||||
@ -25,16 +29,42 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DOMAIN = 'hassio'
|
DOMAIN = 'hassio'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
SERVICE_ADDON_START = 'addon_start'
|
||||||
|
SERVICE_ADDON_STOP = 'addon_stop'
|
||||||
|
SERVICE_ADDON_RESTART = 'addon_restart'
|
||||||
|
SERVICE_ADDON_STDIN = 'addon_stdin'
|
||||||
|
|
||||||
|
ATTR_ADDON = 'addon'
|
||||||
|
ATTR_INPUT = 'input'
|
||||||
|
|
||||||
NO_TIMEOUT = {
|
NO_TIMEOUT = {
|
||||||
re.compile(r'^homeassistant/update$'), re.compile(r'^host/update$'),
|
re.compile(r'^homeassistant/update$'),
|
||||||
re.compile(r'^supervisor/update$'), re.compile(r'^addons/[^/]*/update$'),
|
re.compile(r'^host/update$'),
|
||||||
re.compile(r'^addons/[^/]*/install$')
|
re.compile(r'^supervisor/update$'),
|
||||||
|
re.compile(r'^addons/[^/]*/update$'),
|
||||||
|
re.compile(r'^addons/[^/]*/install$'),
|
||||||
|
re.compile(r'^addons/[^/]*/rebuild$')
|
||||||
}
|
}
|
||||||
|
|
||||||
NO_AUTH = {
|
NO_AUTH = {
|
||||||
re.compile(r'^panel$'), re.compile(r'^addons/[^/]*/logo$')
|
re.compile(r'^panel$'), re.compile(r'^addons/[^/]*/logo$')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCHEMA_ADDON = vol.Schema({
|
||||||
|
vol.Required(ATTR_ADDON): cv.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend({
|
||||||
|
vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)
|
||||||
|
})
|
||||||
|
|
||||||
|
MAP_SERVICE_API = {
|
||||||
|
SERVICE_ADDON_START: ('/addons/{addon}/start', SCHEMA_ADDON),
|
||||||
|
SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON),
|
||||||
|
SERVICE_ADDON_RESTART: ('/addons/{addon}/restart', SCHEMA_ADDON),
|
||||||
|
SERVICE_ADDON_STDIN: ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
@ -48,8 +78,7 @@ def async_setup(hass, config):
|
|||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
hassio = HassIO(hass.loop, websession, host)
|
hassio = HassIO(hass.loop, websession, host)
|
||||||
|
|
||||||
api_ok = yield from hassio.is_connected()
|
if not (yield from hassio.is_connected()):
|
||||||
if not api_ok:
|
|
||||||
_LOGGER.error("Not connected with HassIO!")
|
_LOGGER.error("Not connected with HassIO!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -59,6 +88,23 @@ def async_setup(hass, config):
|
|||||||
register_built_in_panel(hass, 'hassio', 'Hass.io',
|
register_built_in_panel(hass, 'hassio', 'Hass.io',
|
||||||
'mdi:access-point-network')
|
'mdi:access-point-network')
|
||||||
|
|
||||||
|
if 'http' in config:
|
||||||
|
yield from hassio.update_hass_api(config.get('http'))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_service_handler(service):
|
||||||
|
"""Handle service calls for HassIO."""
|
||||||
|
api_command = MAP_SERVICE_API[service.service][0]
|
||||||
|
addon = service.data[ATTR_ADDON]
|
||||||
|
data = service.data[ATTR_INPUT] if ATTR_INPUT in service.data else None
|
||||||
|
|
||||||
|
yield from hassio.send_command(
|
||||||
|
api_command.format(addon=addon), payload=data, timeout=60)
|
||||||
|
|
||||||
|
for service, settings in MAP_SERVICE_API.items():
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, service, async_service_handler, schema=settings[1])
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -71,30 +117,55 @@ 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.
|
||||||
|
|
||||||
|
This method return a coroutine.
|
||||||
|
"""
|
||||||
|
return self.send_command("/supervisor/ping", method="get")
|
||||||
|
|
||||||
|
def update_hass_api(self, http_config):
|
||||||
|
"""Update Home-Assistant API data on HassIO.
|
||||||
|
|
||||||
|
This method return a coroutine.
|
||||||
|
"""
|
||||||
|
options = {
|
||||||
|
'ssl': CONF_SSL_CERTIFICATE in http_config,
|
||||||
|
}
|
||||||
|
|
||||||
|
if http_config.get(CONF_SERVER_PORT):
|
||||||
|
options['port'] = http_config[CONF_SERVER_PORT]
|
||||||
|
|
||||||
|
if http_config.get(CONF_API_PASSWORD):
|
||||||
|
options['password'] = http_config[CONF_API_PASSWORD]
|
||||||
|
|
||||||
|
return self.send_command("/homeassistant/options", payload=options)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def send_command(self, command, method="post", payload=None, timeout=10):
|
||||||
|
"""Send API command to HassIO.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10, loop=self.loop):
|
with async_timeout.timeout(timeout, loop=self.loop):
|
||||||
request = yield from self.websession.get(
|
request = yield from self.websession.request(
|
||||||
"http://{}{}".format(self._ip, "/supervisor/ping")
|
method, "http://{}{}".format(self._ip, command),
|
||||||
)
|
json=payload)
|
||||||
|
|
||||||
if request.status != 200:
|
if request.status != 200:
|
||||||
_LOGGER.error("Ping return code %d.", request.status)
|
_LOGGER.error(
|
||||||
|
"%s return code %d.", command, request.status)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
answer = yield from request.json()
|
answer = yield from request.json()
|
||||||
return answer and answer['result'] == 'ok'
|
return answer and answer['result'] == 'ok'
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error("Timeout on ping request")
|
_LOGGER.error("Timeout on %s request", command)
|
||||||
|
|
||||||
except aiohttp.ClientError as err:
|
except aiohttp.ClientError as err:
|
||||||
_LOGGER.error("Client error on ping request %s", err)
|
_LOGGER.error("Client error on %s request %s", command, err)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -51,6 +51,102 @@ def test_fail_setup_cannot_connect(hass):
|
|||||||
assert not result
|
assert not result
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_setup_api_ping(hass, aioclient_mock):
|
||||||
|
"""Test setup with API ping."""
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://127.0.0.1/supervisor/ping", json={'result': 'ok'})
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}):
|
||||||
|
result = yield from async_setup_component(hass, 'hassio', {})
|
||||||
|
assert result
|
||||||
|
|
||||||
|
assert aioclient_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_setup_api_push_api_data(hass, aioclient_mock):
|
||||||
|
"""Test setup with API push."""
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://127.0.0.1/supervisor/ping", json={'result': 'ok'})
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://127.0.0.1/homeassistant/options", json={'result': 'ok'})
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}):
|
||||||
|
result = yield from async_setup_component(hass, 'hassio', {
|
||||||
|
'http': {
|
||||||
|
'api_password': "123456",
|
||||||
|
'server_port': 9999
|
||||||
|
},
|
||||||
|
'hassio': {}
|
||||||
|
})
|
||||||
|
assert result
|
||||||
|
|
||||||
|
assert aioclient_mock.call_count == 2
|
||||||
|
assert not aioclient_mock.mock_calls[-1][2]['ssl']
|
||||||
|
assert aioclient_mock.mock_calls[-1][2]['password'] == "123456"
|
||||||
|
assert aioclient_mock.mock_calls[-1][2]['port'] == 9999
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_setup_api_push_api_data_default(hass, aioclient_mock):
|
||||||
|
"""Test setup with API push default data."""
|
||||||
|
aioclient_mock.get(
|
||||||
|
"http://127.0.0.1/supervisor/ping", json={'result': 'ok'})
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://127.0.0.1/homeassistant/options", json={'result': 'ok'})
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}):
|
||||||
|
result = yield from async_setup_component(hass, 'hassio', {
|
||||||
|
'http': {},
|
||||||
|
'hassio': {}
|
||||||
|
})
|
||||||
|
assert result
|
||||||
|
|
||||||
|
assert aioclient_mock.call_count == 2
|
||||||
|
assert not aioclient_mock.mock_calls[-1][2]['ssl']
|
||||||
|
assert 'password' not in aioclient_mock.mock_calls[-1][2]
|
||||||
|
assert 'port' not in aioclient_mock.mock_calls[-1][2]
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_service_register(hassio_env, hass):
|
||||||
|
"""Check if service will be settup."""
|
||||||
|
assert (yield from async_setup_component(hass, 'hassio', {}))
|
||||||
|
assert hass.services.has_service('hassio', 'addon_start')
|
||||||
|
assert hass.services.has_service('hassio', 'addon_stop')
|
||||||
|
assert hass.services.has_service('hassio', 'addon_restart')
|
||||||
|
assert hass.services.has_service('hassio', 'addon_stdin')
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_service_calls(hassio_env, hass, aioclient_mock):
|
||||||
|
"""Call service and check the API calls behind that."""
|
||||||
|
assert (yield from async_setup_component(hass, 'hassio', {}))
|
||||||
|
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://127.0.0.1/addons/test/start", json={'result': 'ok'})
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://127.0.0.1/addons/test/stop", json={'result': 'ok'})
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://127.0.0.1/addons/test/restart", json={'result': 'ok'})
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://127.0.0.1/addons/test/stdin", json={'result': 'ok'})
|
||||||
|
|
||||||
|
yield from hass.services.async_call(
|
||||||
|
'hassio', 'addon_start', {'addon': 'test'})
|
||||||
|
yield from hass.services.async_call(
|
||||||
|
'hassio', 'addon_stop', {'addon': 'test'})
|
||||||
|
yield from hass.services.async_call(
|
||||||
|
'hassio', 'addon_restart', {'addon': 'test'})
|
||||||
|
yield from hass.services.async_call(
|
||||||
|
'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'})
|
||||||
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert aioclient_mock.call_count == 4
|
||||||
|
assert aioclient_mock.mock_calls[-1][2] == 'test'
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_forward_request(hassio_client):
|
def test_forward_request(hassio_client):
|
||||||
"""Test fetching normal path."""
|
"""Test fetching normal path."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user