Add an option to serve ES6 JS to clients (#10474)

* Add an option to serve ES6 JS to clients

* Rename es6 to latest

* Fixes

* Serve JS vrsions from separate dirs

* Revert websocket API change

* Update frontend to 20171110.0

* websocket: move request to constructor
This commit is contained in:
Andrey 2017-11-11 09:02:06 +02:00 committed by Paulus Schoutsen
parent 1c36e2f586
commit 5e92fa3404
10 changed files with 150 additions and 65 deletions

View File

@ -9,6 +9,7 @@ import hashlib
import json import json
import logging import logging
import os import os
from urllib.parse import urlparse
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
@ -21,21 +22,19 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171106.0'] REQUIREMENTS = ['home-assistant-frontend==20171110.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api'] DEPENDENCIES = ['api', 'websocket_api', 'http']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
POLYMER_PATH = os.path.join(os.path.dirname(__file__),
'home-assistant-polymer/')
FINAL_PATH = os.path.join(POLYMER_PATH, 'final')
CONF_THEMES = 'themes' CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_FRONTEND_REPO = 'development_repo' CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5'
JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4' DEFAULT_THEME_COLOR = '#03A9F4'
@ -61,6 +60,7 @@ for size in (192, 384, 512, 1024):
DATA_FINALIZE_PANEL = 'frontend_finalize_panel' DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels' DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_THEMES = 'frontend_themes' DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme' DATA_DEFAULT_THEME = 'frontend_default_theme'
@ -68,8 +68,6 @@ DEFAULT_THEME = 'default'
PRIMARY_COLOR = 'primary-color' PRIMARY_COLOR = 'primary-color'
# To keep track we don't register a component twice (gives a warning)
# _REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -80,6 +78,8 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
vol.Optional(CONF_EXTRA_HTML_URL): vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
vol.In(JS_OPTIONS)
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -102,8 +102,9 @@ class AbstractPanel:
# Title to show in the sidebar (optional) # Title to show in the sidebar (optional)
sidebar_title = None sidebar_title = None
# Url to the webcomponent # Url to the webcomponent (depending on JS version)
webcomponent_url = None webcomponent_url_es5 = None
webcomponent_url_latest = None
# Url to show the panel in the frontend # Url to show the panel in the frontend
frontend_url_path = None frontend_url_path = None
@ -135,16 +136,20 @@ class AbstractPanel:
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get) index_view.get)
def as_dict(self): def to_response(self, hass, request):
"""Panel as dictionary.""" """Panel as dictionary."""
return { result = {
'component_name': self.component_name, 'component_name': self.component_name,
'icon': self.sidebar_icon, 'icon': self.sidebar_icon,
'title': self.sidebar_title, 'title': self.sidebar_title,
'url': self.webcomponent_url,
'url_path': self.frontend_url_path, 'url_path': self.frontend_url_path,
'config': self.config, 'config': self.config,
} }
if _is_latest(hass.data[DATA_JS_VERSION], request):
result['url'] = self.webcomponent_url_latest
else:
result['url'] = self.webcomponent_url_es5
return result
class BuiltInPanel(AbstractPanel): class BuiltInPanel(AbstractPanel):
@ -170,15 +175,19 @@ class BuiltInPanel(AbstractPanel):
if frontend_repository_path is None: if frontend_repository_path is None:
import hass_frontend import hass_frontend
import hass_frontend_es5
self.webcomponent_url = \ self.webcomponent_url_latest = \
'/static/panels/ha-panel-{}-{}.html'.format( '/frontend_latest/panels/ha-panel-{}-{}.html'.format(
self.component_name, self.component_name,
hass_frontend.FINGERPRINTS[panel_path]) hass_frontend.FINGERPRINTS[panel_path])
self.webcomponent_url_es5 = \
'/frontend_es5/panels/ha-panel-{}-{}.html'.format(
self.component_name,
hass_frontend_es5.FINGERPRINTS[panel_path])
else: else:
# Dev mode # Dev mode
self.webcomponent_url = \ self.webcomponent_url_es5 = self.webcomponent_url_latest = \
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format( '/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
self.component_name, self.component_name) self.component_name, self.component_name)
@ -208,18 +217,20 @@ class ExternalPanel(AbstractPanel):
""" """
try: try:
if self.md5 is None: if self.md5 is None:
yield from hass.async_add_job(_fingerprint, self.path) self.md5 = yield from hass.async_add_job(
_fingerprint, self.path)
except OSError: except OSError:
_LOGGER.error('Cannot find or access %s at %s', _LOGGER.error('Cannot find or access %s at %s',
self.component_name, self.path) self.component_name, self.path)
hass.data[DATA_PANELS].pop(self.frontend_url_path) hass.data[DATA_PANELS].pop(self.frontend_url_path)
return
self.webcomponent_url = \ self.webcomponent_url_es5 = self.webcomponent_url_latest = \
URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5) URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
if self.component_name not in self.REGISTERED_COMPONENTS: if self.component_name not in self.REGISTERED_COMPONENTS:
hass.http.register_static_path( hass.http.register_static_path(
self.webcomponent_url, self.path, self.webcomponent_url_latest, self.path,
# if path is None, we're in prod mode, so cache static assets # if path is None, we're in prod mode, so cache static assets
frontend_repository_path is None) frontend_repository_path is None)
self.REGISTERED_COMPONENTS.add(self.component_name) self.REGISTERED_COMPONENTS.add(self.component_name)
@ -281,31 +292,50 @@ def async_setup(hass, config):
repo_path = conf.get(CONF_FRONTEND_REPO) repo_path = conf.get(CONF_FRONTEND_REPO)
is_dev = repo_path is not None is_dev = repo_path is not None
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev: if is_dev:
hass.http.register_static_path( hass.http.register_static_path(
"/home-assistant-polymer", repo_path, False) "/home-assistant-polymer", repo_path, False)
hass.http.register_static_path( hass.http.register_static_path(
"/static/translations", "/static/translations",
os.path.join(repo_path, "build/translations"), False) os.path.join(repo_path, "build-translations"), False)
sw_path = os.path.join(repo_path, "build/service_worker.js") sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
static_path = os.path.join(repo_path, 'hass_frontend') static_path = os.path.join(repo_path, 'hass_frontend')
frontend_es5_path = os.path.join(repo_path, 'build-es5')
frontend_latest_path = os.path.join(repo_path, 'build')
else: else:
import hass_frontend import hass_frontend
frontend_path = hass_frontend.where() import hass_frontend_es5
sw_path = os.path.join(frontend_path, "service_worker.js") sw_path_es5 = os.path.join(hass_frontend_es5.where(),
static_path = frontend_path "service_worker.js")
sw_path_latest = os.path.join(hass_frontend.where(),
"service_worker.js")
# /static points to dir with files that are JS-type agnostic.
# ES5 files are served from /frontend_es5.
# ES6 files are served from /frontend_latest.
static_path = hass_frontend.where()
frontend_es5_path = hass_frontend_es5.where()
frontend_latest_path = static_path
hass.http.register_static_path("/service_worker.js", sw_path, False) hass.http.register_static_path(
"/service_worker_es5.js", sw_path_es5, False)
hass.http.register_static_path(
"/service_worker.js", sw_path_latest, False)
hass.http.register_static_path( hass.http.register_static_path(
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev) "/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev)
hass.http.register_static_path("/static", static_path, not is_dev) hass.http.register_static_path("/static", static_path, not is_dev)
hass.http.register_static_path(
"/frontend_latest", frontend_latest_path, not is_dev)
hass.http.register_static_path(
"/frontend_es5", frontend_es5_path, not is_dev)
local = hass.config.path('www') local = hass.config.path('www')
if os.path.isdir(local): if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev) hass.http.register_static_path("/local", local, not is_dev)
index_view = IndexView(is_dev) index_view = IndexView(is_dev, js_version)
hass.http.register_view(index_view) hass.http.register_view(index_view)
@asyncio.coroutine @asyncio.coroutine
@ -405,7 +435,7 @@ class IndexView(HomeAssistantView):
requires_auth = False requires_auth = False
extra_urls = ['/states', '/states/{extra}'] extra_urls = ['/states', '/states/{extra}']
def __init__(self, use_repo): def __init__(self, use_repo, js_option):
"""Initialize the frontend view.""" """Initialize the frontend view."""
from jinja2 import FileSystemLoader, Environment from jinja2 import FileSystemLoader, Environment
@ -416,27 +446,37 @@ class IndexView(HomeAssistantView):
os.path.join(os.path.dirname(__file__), 'templates/') os.path.join(os.path.dirname(__file__), 'templates/')
) )
) )
self.js_option = js_option
@asyncio.coroutine @asyncio.coroutine
def get(self, request, extra=None): def get(self, request, extra=None):
"""Serve the index view.""" """Serve the index view."""
hass = request.app['hass'] hass = request.app['hass']
latest = _is_latest(self.js_option, request)
compatibility_url = None
if self.use_repo: if self.use_repo:
core_url = '/home-assistant-polymer/build/core.js' core_url = '/home-assistant-polymer/{}/core.js'.format(
compatibility_url = \ 'build' if latest else 'build-es5')
'/home-assistant-polymer/build/compatibility.js'
ui_url = '/home-assistant-polymer/src/home-assistant.html' ui_url = '/home-assistant-polymer/src/home-assistant.html'
icons_fp = '' icons_fp = ''
icons_url = '/static/mdi.html' icons_url = '/static/mdi.html'
else: else:
if latest:
import hass_frontend
core_url = '/frontend_latest/core-{}.js'.format(
hass_frontend.FINGERPRINTS['core.js'])
ui_url = '/frontend_latest/frontend-{}.html'.format(
hass_frontend.FINGERPRINTS['frontend.html'])
else:
import hass_frontend_es5
core_url = '/frontend_es5/core-{}.js'.format(
hass_frontend_es5.FINGERPRINTS['core.js'])
compatibility_url = '/frontend_es5/compatibility-{}.js'.format(
hass_frontend_es5.FINGERPRINTS['compatibility.js'])
ui_url = '/frontend_es5/frontend-{}.html'.format(
hass_frontend_es5.FINGERPRINTS['frontend.html'])
import hass_frontend import hass_frontend
core_url = '/static/core-{}.js'.format(
hass_frontend.FINGERPRINTS['core.js'])
compatibility_url = '/static/compatibility-{}.js'.format(
hass_frontend.FINGERPRINTS['compatibility.js'])
ui_url = '/static/frontend-{}.html'.format(
hass_frontend.FINGERPRINTS['frontend.html'])
icons_fp = '-{}'.format(hass_frontend.FINGERPRINTS['mdi.html']) icons_fp = '-{}'.format(hass_frontend.FINGERPRINTS['mdi.html'])
icons_url = '/static/mdi{}.html'.format(icons_fp) icons_url = '/static/mdi{}.html'.format(icons_fp)
@ -447,8 +487,10 @@ class IndexView(HomeAssistantView):
if panel == 'states': if panel == 'states':
panel_url = '' panel_url = ''
elif latest:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
else: else:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = 'true' no_auth = 'true'
if hass.config.api.api_password and not is_trusted_ip(request): if hass.config.api.api_password and not is_trusted_ip(request):
@ -468,7 +510,10 @@ class IndexView(HomeAssistantView):
panel_url=panel_url, panels=hass.data[DATA_PANELS], panel_url=panel_url, panels=hass.data[DATA_PANELS],
dev_mode=self.use_repo, dev_mode=self.use_repo,
theme_color=MANIFEST_JSON['theme_color'], theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[DATA_EXTRA_HTML_URL]) extra_urls=hass.data[DATA_EXTRA_HTML_URL],
latest=latest,
service_worker_name='/service_worker.js' if latest else
'/service_worker_es5.js')
return web.Response(text=resp, content_type='text/html') return web.Response(text=resp, content_type='text/html')
@ -509,3 +554,20 @@ def _fingerprint(path):
"""Fingerprint a file.""" """Fingerprint a file."""
with open(path) as fil: with open(path) as fil:
return hashlib.md5(fil.read().encode('utf-8')).hexdigest() return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
def _is_latest(js_option, request):
"""
Return whether we should serve latest untranspiled code.
Set according to user's preference and URL override.
"""
if request is None:
return js_option == 'latest'
latest_in_query = 'latest' in request.query or (
request.headers.get('Referer') and
'latest' in urlparse(request.headers['Referer']).query)
es5_in_query = 'es5' in request.query or (
request.headers.get('Referer') and
'es5' in urlparse(request.headers['Referer']).query)
return latest_in_query or (not es5_in_query and js_option == 'latest')

View File

@ -78,11 +78,11 @@
<a href='/'>TRY AGAIN</a> <a href='/'>TRY AGAIN</a>
</div> </div>
</div> </div>
<home-assistant icons='{{ icons }}'></home-assistant> <home-assistant></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #} {# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> -#}
{% if not latest -%}
<script> <script>
var compatibilityRequired = ( var compatibilityRequired = (typeof Object.assign != 'function');
typeof Object.assign != 'function');
if (compatibilityRequired) { if (compatibilityRequired) {
var e = document.createElement('script'); var e = document.createElement('script');
e.onerror = initError; e.onerror = initError;
@ -90,10 +90,11 @@
document.head.appendChild(e); document.head.appendChild(e);
} }
</script> </script>
{% endif -%}
<script src='{{ core_url }}'></script> <script src='{{ core_url }}'></script>
{% if not dev_mode %} {% if not dev_mode and not latest -%}
<script src='/static/custom-elements-es5-adapter.js'></script> <script src='/frontend_es5/custom-elements-es5-adapter.js'></script>
{% endif %} {% endif -%}
<script> <script>
var webComponentsSupported = ( var webComponentsSupported = (
'customElements' in window && 'customElements' in window &&
@ -105,6 +106,11 @@
e.src = '/static/webcomponents-lite.js'; e.src = '/static/webcomponents-lite.js';
document.head.appendChild(e); document.head.appendChild(e);
} }
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('{{ service_worker_name }}');
});
}
</script> </script>
<link rel='import' href='{{ ui_url }}' onerror='initError()'> <link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url -%} {% if panel_url -%}

View File

@ -262,7 +262,6 @@ class HomeAssistantWSGI(object):
resource = CachingStaticResource resource = CachingStaticResource
else: else:
resource = web.StaticResource resource = web.StaticResource
self.app.router.register_resource(resource(url_path, path)) self.app.router.register_resource(resource(url_path, path))
return return

View File

@ -65,7 +65,8 @@ class CachingFileResponse(FileResponse):
@asyncio.coroutine @asyncio.coroutine
def staticresource_middleware(request, handler): def staticresource_middleware(request, handler):
"""Middleware to strip out fingerprint from fingerprinted assets.""" """Middleware to strip out fingerprint from fingerprinted assets."""
if not request.path.startswith('/static/'): path = request.path
if not path.startswith('/static/') and not path.startswith('/frontend'):
return handler(request) return handler(request)
fingerprinted = _FINGERPRINT.match(request.match_info['filename']) fingerprinted = _FINGERPRINT.match(request.match_info['filename'])

View File

@ -202,15 +202,16 @@ class WebsocketAPIView(HomeAssistantView):
def get(self, request): def get(self, request):
"""Handle an incoming websocket connection.""" """Handle an incoming websocket connection."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
return ActiveConnection(request.app['hass']).handle(request) return ActiveConnection(request.app['hass'], request).handle()
class ActiveConnection: class ActiveConnection:
"""Handle an active websocket client connection.""" """Handle an active websocket client connection."""
def __init__(self, hass): def __init__(self, hass, request):
"""Initialize an active connection.""" """Initialize an active connection."""
self.hass = hass self.hass = hass
self.request = request
self.wsock = None self.wsock = None
self.event_listeners = {} self.event_listeners = {}
self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop)
@ -259,8 +260,9 @@ class ActiveConnection:
self._writer_task.cancel() self._writer_task.cancel()
@asyncio.coroutine @asyncio.coroutine
def handle(self, request): def handle(self):
"""Handle the websocket connection.""" """Handle the websocket connection."""
request = self.request
wsock = self.wsock = web.WebSocketResponse() wsock = self.wsock = web.WebSocketResponse()
yield from wsock.prepare(request) yield from wsock.prepare(request)
self.debug("Connected") self.debug("Connected")
@ -350,7 +352,7 @@ class ActiveConnection:
if wsock.closed: if wsock.closed:
self.debug("Connection closed by client") self.debug("Connection closed by client")
else: else:
self.log_error("Unexpected TypeError", msg) _LOGGER.exception("Unexpected TypeError: %s", msg)
except ValueError as err: except ValueError as err:
msg = "Received invalid JSON" msg = "Received invalid JSON"
@ -483,9 +485,14 @@ class ActiveConnection:
Async friendly. Async friendly.
""" """
msg = GET_PANELS_MESSAGE_SCHEMA(msg) msg = GET_PANELS_MESSAGE_SCHEMA(msg)
panels = {
panel:
self.hass.data[frontend.DATA_PANELS][panel].to_response(
self.hass, self.request)
for panel in self.hass.data[frontend.DATA_PANELS]}
self.to_write.put_nowait(result_message( self.to_write.put_nowait(result_message(
msg['id'], self.hass.data[frontend.DATA_PANELS])) msg['id'], panels))
def handle_ping(self, msg): def handle_ping(self, msg):
"""Handle ping command. """Handle ping command.

View File

@ -330,7 +330,7 @@ hipnotify==1.0.8
holidays==0.8.1 holidays==0.8.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20171106.0 home-assistant-frontend==20171110.0
# homeassistant.components.camera.onvif # homeassistant.components.camera.onvif
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a

View File

@ -74,7 +74,7 @@ hbmqtt==0.8
holidays==0.8.1 holidays==0.8.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20171106.0 home-assistant-frontend==20171110.0
# homeassistant.components.influxdb # homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb # homeassistant.components.sensor.influxdb

View File

@ -52,7 +52,7 @@ def test_frontend_and_static(mock_http_client):
# Test we can retrieve frontend.js # Test we can retrieve frontend.js
frontendjs = re.search( frontendjs = re.search(
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)', text) r'(?P<app>\/frontend_es5\/frontend-[A-Za-z0-9]{32}.html)', text)
assert frontendjs is not None assert frontendjs is not None
resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) resp = yield from mock_http_client.get(frontendjs.groups(0)[0])
@ -63,6 +63,10 @@ def test_frontend_and_static(mock_http_client):
@asyncio.coroutine @asyncio.coroutine
def test_dont_cache_service_worker(mock_http_client): def test_dont_cache_service_worker(mock_http_client):
"""Test that we don't cache the service worker.""" """Test that we don't cache the service worker."""
resp = yield from mock_http_client.get('/service_worker_es5.js')
assert resp.status == 200
assert 'cache-control' not in resp.headers
resp = yield from mock_http_client.get('/service_worker.js') resp = yield from mock_http_client.get('/service_worker.js')
assert resp.status == 200 assert resp.status == 200
assert 'cache-control' not in resp.headers assert 'cache-control' not in resp.headers

View File

@ -33,7 +33,7 @@ class TestPanelIframe(unittest.TestCase):
'panel_iframe': conf 'panel_iframe': conf
}) })
@patch.dict('hass_frontend.FINGERPRINTS', @patch.dict('hass_frontend_es5.FINGERPRINTS',
{'panels/ha-panel-iframe.html': 'md5md5'}) {'panels/ha-panel-iframe.html': 'md5md5'})
def test_correct_config(self): def test_correct_config(self):
"""Test correct config.""" """Test correct config."""
@ -55,20 +55,20 @@ class TestPanelIframe(unittest.TestCase):
panels = self.hass.data[frontend.DATA_PANELS] panels = self.hass.data[frontend.DATA_PANELS]
assert panels.get('router').as_dict() == { assert panels.get('router').to_response(self.hass, None) == {
'component_name': 'iframe', 'component_name': 'iframe',
'config': {'url': 'http://192.168.1.1'}, 'config': {'url': 'http://192.168.1.1'},
'icon': 'mdi:network-wireless', 'icon': 'mdi:network-wireless',
'title': 'Router', 'title': 'Router',
'url': '/static/panels/ha-panel-iframe-md5md5.html', 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'router' 'url_path': 'router'
} }
assert panels.get('weather').as_dict() == { assert panels.get('weather').to_response(self.hass, None) == {
'component_name': 'iframe', 'component_name': 'iframe',
'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'},
'icon': 'mdi:weather', 'icon': 'mdi:weather',
'title': 'Weather', 'title': 'Weather',
'url': '/static/panels/ha-panel-iframe-md5md5.html', 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'weather', 'url_path': 'weather',
} }

View File

@ -290,7 +290,7 @@ def test_get_panels(hass, websocket_client):
"""Test get_panels command.""" """Test get_panels command."""
yield from hass.components.frontend.async_register_built_in_panel( yield from hass.components.frontend.async_register_built_in_panel(
'map', 'Map', 'mdi:account-location') 'map', 'Map', 'mdi:account-location')
hass.data[frontend.DATA_JS_VERSION] = 'es5'
websocket_client.send_json({ websocket_client.send_json({
'id': 5, 'id': 5,
'type': wapi.TYPE_GET_PANELS, 'type': wapi.TYPE_GET_PANELS,
@ -300,8 +300,14 @@ def test_get_panels(hass, websocket_client):
assert msg['id'] == 5 assert msg['id'] == 5
assert msg['type'] == wapi.TYPE_RESULT assert msg['type'] == wapi.TYPE_RESULT
assert msg['success'] assert msg['success']
assert msg['result'] == {url: panel.as_dict() for url, panel assert msg['result'] == {'map': {
in hass.data[frontend.DATA_PANELS].items()} 'component_name': 'map',
'url_path': 'map',
'config': None,
'url': None,
'icon': 'mdi:account-location',
'title': 'Map',
}}
@asyncio.coroutine @asyncio.coroutine