mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Custom panel (#14708)
* Add support for custom panels in JS * Allow specifying JS custom panels * Add trust external option * Fix tests * Do I/O outside event loop * Change config to avoid breaking change
This commit is contained in:
parent
ab3717af76
commit
f6eb9e79d5
@ -4,7 +4,6 @@ Register a custom front end panel.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/panel_custom/
|
https://home-assistant.io/components/panel_custom/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -21,27 +20,33 @@ CONF_SIDEBAR_ICON = 'sidebar_icon'
|
|||||||
CONF_URL_PATH = 'url_path'
|
CONF_URL_PATH = 'url_path'
|
||||||
CONF_CONFIG = 'config'
|
CONF_CONFIG = 'config'
|
||||||
CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
|
CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
|
||||||
|
CONF_JS_URL = 'js_url'
|
||||||
|
CONF_EMBED_IFRAME = 'embed_iframe'
|
||||||
|
CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script'
|
||||||
|
|
||||||
DEFAULT_ICON = 'mdi:bookmark'
|
DEFAULT_ICON = 'mdi:bookmark'
|
||||||
|
LEGACY_URL = '/api/panel_custom/{}'
|
||||||
|
|
||||||
PANEL_DIR = 'panels'
|
PANEL_DIR = 'panels'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.All(cv.ensure_list, [{
|
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||||
vol.Required(CONF_COMPONENT_NAME): cv.slug,
|
vol.Required(CONF_COMPONENT_NAME): cv.string,
|
||||||
vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
|
vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
|
||||||
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
|
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
|
||||||
vol.Optional(CONF_URL_PATH): cv.string,
|
vol.Optional(CONF_URL_PATH): cv.string,
|
||||||
vol.Optional(CONF_CONFIG): cv.match_all,
|
vol.Optional(CONF_CONFIG): dict,
|
||||||
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
|
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
|
||||||
}])
|
vol.Optional(CONF_JS_URL): cv.string,
|
||||||
|
vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean,
|
||||||
|
})])
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Initialize custom panel."""
|
"""Initialize custom panel."""
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
@ -52,17 +57,39 @@ def async_setup(hass, config):
|
|||||||
if panel_path is None:
|
if panel_path is None:
|
||||||
panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
|
panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
|
||||||
|
|
||||||
if not os.path.isfile(panel_path):
|
custom_panel_config = {
|
||||||
|
'name': name,
|
||||||
|
'embed_iframe': panel[CONF_EMBED_IFRAME],
|
||||||
|
'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONF_JS_URL in panel:
|
||||||
|
custom_panel_config['js_url'] = panel[CONF_JS_URL]
|
||||||
|
|
||||||
|
elif not await hass.async_add_job(os.path.isfile, panel_path):
|
||||||
_LOGGER.error('Unable to find webcomponent for %s: %s',
|
_LOGGER.error('Unable to find webcomponent for %s: %s',
|
||||||
name, panel_path)
|
name, panel_path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
yield from hass.components.frontend.async_register_panel(
|
else:
|
||||||
name, panel_path,
|
url = LEGACY_URL.format(name)
|
||||||
|
hass.http.register_static_path(url, panel_path)
|
||||||
|
custom_panel_config['html_url'] = LEGACY_URL.format(name)
|
||||||
|
|
||||||
|
if CONF_CONFIG in panel:
|
||||||
|
# Make copy because we're mutating it
|
||||||
|
config = dict(panel[CONF_CONFIG])
|
||||||
|
else:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
config['_panel_custom'] = custom_panel_config
|
||||||
|
|
||||||
|
await hass.components.frontend.async_register_built_in_panel(
|
||||||
|
component_name='custom',
|
||||||
sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
|
sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
|
||||||
sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
|
sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
|
||||||
frontend_url_path=panel.get(CONF_URL_PATH),
|
frontend_url_path=panel.get(CONF_URL_PATH),
|
||||||
config=panel.get(CONF_CONFIG),
|
config=config
|
||||||
)
|
)
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
|
@ -1,23 +1,11 @@
|
|||||||
"""The tests for the panel_custom component."""
|
"""The tests for the panel_custom component."""
|
||||||
import asyncio
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
from homeassistant.components import frontend
|
from homeassistant.components import frontend
|
||||||
|
|
||||||
from tests.common import mock_component
|
|
||||||
|
|
||||||
|
async def test_webcomponent_custom_path_not_found(hass):
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def mock_frontend_loaded(hass):
|
|
||||||
"""Mock frontend is loaded."""
|
|
||||||
mock_component(hass, 'frontend')
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def test_webcomponent_custom_path_not_found(hass):
|
|
||||||
"""Test if a web component is found in config panels dir."""
|
"""Test if a web component is found in config panels dir."""
|
||||||
filename = 'mock.file'
|
filename = 'mock.file'
|
||||||
|
|
||||||
@ -33,45 +21,96 @@ def test_webcomponent_custom_path_not_found(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with patch('os.path.isfile', Mock(return_value=False)):
|
with patch('os.path.isfile', Mock(return_value=False)):
|
||||||
result = yield from setup.async_setup_component(
|
result = await setup.async_setup_component(
|
||||||
hass, 'panel_custom', config
|
hass, 'panel_custom', config
|
||||||
)
|
)
|
||||||
assert not result
|
assert not result
|
||||||
assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0
|
assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_webcomponent_custom_path(hass):
|
||||||
def test_webcomponent_custom_path(hass):
|
|
||||||
"""Test if a web component is found in config panels dir."""
|
"""Test if a web component is found in config panels dir."""
|
||||||
filename = 'mock.file'
|
filename = 'mock.file'
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
'panel_custom': {
|
'panel_custom': {
|
||||||
'name': 'todomvc',
|
'name': 'todo-mvc',
|
||||||
'webcomponent_path': filename,
|
'webcomponent_path': filename,
|
||||||
'sidebar_title': 'Sidebar Title',
|
'sidebar_title': 'Sidebar Title',
|
||||||
'sidebar_icon': 'mdi:iconicon',
|
'sidebar_icon': 'mdi:iconicon',
|
||||||
'url_path': 'nice_url',
|
'url_path': 'nice_url',
|
||||||
'config': 5,
|
'config': {
|
||||||
|
'hello': 'world',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch('os.path.isfile', Mock(return_value=True)):
|
with patch('os.path.isfile', Mock(return_value=True)):
|
||||||
with patch('os.access', Mock(return_value=True)):
|
with patch('os.access', Mock(return_value=True)):
|
||||||
result = yield from setup.async_setup_component(
|
result = await setup.async_setup_component(
|
||||||
hass, 'panel_custom', config
|
hass, 'panel_custom', config
|
||||||
)
|
)
|
||||||
assert result
|
assert result
|
||||||
|
|
||||||
panels = hass.data.get(frontend.DATA_PANELS, [])
|
panels = hass.data.get(frontend.DATA_PANELS, [])
|
||||||
|
|
||||||
assert len(panels) == 1
|
assert panels
|
||||||
assert 'nice_url' in panels
|
assert 'nice_url' in panels
|
||||||
|
|
||||||
panel = panels['nice_url']
|
panel = panels['nice_url']
|
||||||
|
|
||||||
assert panel.config == 5
|
assert panel.config == {
|
||||||
|
'hello': 'world',
|
||||||
|
'_panel_custom': {
|
||||||
|
'html_url': '/api/panel_custom/todo-mvc',
|
||||||
|
'name': 'todo-mvc',
|
||||||
|
'embed_iframe': False,
|
||||||
|
'trust_external': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert panel.frontend_url_path == 'nice_url'
|
||||||
|
assert panel.sidebar_icon == 'mdi:iconicon'
|
||||||
|
assert panel.sidebar_title == 'Sidebar Title'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_js_webcomponent(hass):
|
||||||
|
"""Test if a web component is found in config panels dir."""
|
||||||
|
config = {
|
||||||
|
'panel_custom': {
|
||||||
|
'name': 'todo-mvc',
|
||||||
|
'js_url': '/local/bla.js',
|
||||||
|
'sidebar_title': 'Sidebar Title',
|
||||||
|
'sidebar_icon': 'mdi:iconicon',
|
||||||
|
'url_path': 'nice_url',
|
||||||
|
'config': {
|
||||||
|
'hello': 'world',
|
||||||
|
},
|
||||||
|
'embed_iframe': True,
|
||||||
|
'trust_external_script': True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await setup.async_setup_component(
|
||||||
|
hass, 'panel_custom', config
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
|
||||||
|
panels = hass.data.get(frontend.DATA_PANELS, [])
|
||||||
|
|
||||||
|
assert panels
|
||||||
|
assert 'nice_url' in panels
|
||||||
|
|
||||||
|
panel = panels['nice_url']
|
||||||
|
|
||||||
|
assert panel.config == {
|
||||||
|
'hello': 'world',
|
||||||
|
'_panel_custom': {
|
||||||
|
'js_url': '/local/bla.js',
|
||||||
|
'name': 'todo-mvc',
|
||||||
|
'embed_iframe': True,
|
||||||
|
'trust_external': True,
|
||||||
|
}
|
||||||
|
}
|
||||||
assert panel.frontend_url_path == 'nice_url'
|
assert panel.frontend_url_path == 'nice_url'
|
||||||
assert panel.sidebar_icon == 'mdi:iconicon'
|
assert panel.sidebar_icon == 'mdi:iconicon'
|
||||||
assert panel.sidebar_title == 'Sidebar Title'
|
assert panel.sidebar_title == 'Sidebar Title'
|
||||||
assert panel.path == filename
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user