diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 224970499f3..ba09e60b742 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -9,6 +9,7 @@ import hashlib
import json
import logging
import os
+from urllib.parse import urlparse
from aiohttp import web
import voluptuous as vol
@@ -21,21 +22,19 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['home-assistant-frontend==20171106.0']
+REQUIREMENTS = ['home-assistant-frontend==20171110.0']
DOMAIN = 'frontend'
-DEPENDENCIES = ['api', 'websocket_api']
+DEPENDENCIES = ['api', 'websocket_api', 'http']
-URL_PANEL_COMPONENT = '/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_EXTRA_HTML_URL = 'extra_html_url'
CONF_FRONTEND_REPO = 'development_repo'
+CONF_JS_VERSION = 'javascript_version'
+JS_DEFAULT_OPTION = 'es5'
+JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4'
@@ -61,6 +60,7 @@ for size in (192, 384, 512, 1024):
DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels'
+DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme'
@@ -68,8 +68,6 @@ DEFAULT_THEME = 'default'
PRIMARY_COLOR = 'primary-color'
-# To keep track we don't register a component twice (gives a warning)
-# _REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
@@ -80,6 +78,8 @@ CONFIG_SCHEMA = vol.Schema({
}),
vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
+ vol.In(JS_OPTIONS)
}),
}, extra=vol.ALLOW_EXTRA)
@@ -102,8 +102,9 @@ class AbstractPanel:
# Title to show in the sidebar (optional)
sidebar_title = None
- # Url to the webcomponent
- webcomponent_url = None
+ # Url to the webcomponent (depending on JS version)
+ webcomponent_url_es5 = None
+ webcomponent_url_latest = None
# Url to show the panel in the frontend
frontend_url_path = None
@@ -135,16 +136,20 @@ class AbstractPanel:
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get)
- def as_dict(self):
+ def to_response(self, hass, request):
"""Panel as dictionary."""
- return {
+ result = {
'component_name': self.component_name,
'icon': self.sidebar_icon,
'title': self.sidebar_title,
- 'url': self.webcomponent_url,
'url_path': self.frontend_url_path,
'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):
@@ -170,15 +175,19 @@ class BuiltInPanel(AbstractPanel):
if frontend_repository_path is None:
import hass_frontend
+ import hass_frontend_es5
- self.webcomponent_url = \
- '/static/panels/ha-panel-{}-{}.html'.format(
+ self.webcomponent_url_latest = \
+ '/frontend_latest/panels/ha-panel-{}-{}.html'.format(
self.component_name,
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:
# Dev mode
- self.webcomponent_url = \
+ self.webcomponent_url_es5 = self.webcomponent_url_latest = \
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
self.component_name, self.component_name)
@@ -208,18 +217,20 @@ class ExternalPanel(AbstractPanel):
"""
try:
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:
_LOGGER.error('Cannot find or access %s at %s',
self.component_name, self.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)
if self.component_name not in self.REGISTERED_COMPONENTS:
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
frontend_repository_path is None)
self.REGISTERED_COMPONENTS.add(self.component_name)
@@ -281,31 +292,50 @@ def async_setup(hass, config):
repo_path = conf.get(CONF_FRONTEND_REPO)
is_dev = repo_path is not None
+ hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev:
hass.http.register_static_path(
"/home-assistant-polymer", repo_path, False)
hass.http.register_static_path(
"/static/translations",
- os.path.join(repo_path, "build/translations"), False)
- sw_path = os.path.join(repo_path, "build/service_worker.js")
+ os.path.join(repo_path, "build-translations"), False)
+ 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')
+ frontend_es5_path = os.path.join(repo_path, 'build-es5')
+ frontend_latest_path = os.path.join(repo_path, 'build')
else:
import hass_frontend
- frontend_path = hass_frontend.where()
- sw_path = os.path.join(frontend_path, "service_worker.js")
- static_path = frontend_path
+ import hass_frontend_es5
+ sw_path_es5 = os.path.join(hass_frontend_es5.where(),
+ "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(
"/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(
+ "/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')
if os.path.isdir(local):
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)
@asyncio.coroutine
@@ -405,7 +435,7 @@ class IndexView(HomeAssistantView):
requires_auth = False
extra_urls = ['/states', '/states/{extra}']
- def __init__(self, use_repo):
+ def __init__(self, use_repo, js_option):
"""Initialize the frontend view."""
from jinja2 import FileSystemLoader, Environment
@@ -416,27 +446,37 @@ class IndexView(HomeAssistantView):
os.path.join(os.path.dirname(__file__), 'templates/')
)
)
+ self.js_option = js_option
@asyncio.coroutine
def get(self, request, extra=None):
"""Serve the index view."""
hass = request.app['hass']
+ latest = _is_latest(self.js_option, request)
+ compatibility_url = None
if self.use_repo:
- core_url = '/home-assistant-polymer/build/core.js'
- compatibility_url = \
- '/home-assistant-polymer/build/compatibility.js'
+ core_url = '/home-assistant-polymer/{}/core.js'.format(
+ 'build' if latest else 'build-es5')
ui_url = '/home-assistant-polymer/src/home-assistant.html'
icons_fp = ''
icons_url = '/static/mdi.html'
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
- 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_url = '/static/mdi{}.html'.format(icons_fp)
@@ -447,8 +487,10 @@ class IndexView(HomeAssistantView):
if panel == 'states':
panel_url = ''
+ elif latest:
+ panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
else:
- panel_url = hass.data[DATA_PANELS][panel].webcomponent_url
+ panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = 'true'
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],
dev_mode=self.use_repo,
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')
@@ -509,3 +554,20 @@ def _fingerprint(path):
"""Fingerprint a file."""
with open(path) as fil:
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')
diff --git a/homeassistant/components/frontend/templates/index.html b/homeassistant/components/frontend/templates/index.html
index c941fbc15ae..ae030a5d026 100644
--- a/homeassistant/components/frontend/templates/index.html
+++ b/homeassistant/components/frontend/templates/index.html
@@ -78,11 +78,11 @@
TRY AGAIN
-
- {# #}
+
+ {# -#}
+ {% if not latest -%}
+ {% endif -%}
- {% if not dev_mode %}
-
- {% endif %}
+ {% if not dev_mode and not latest -%}
+
+ {% endif -%}
{% if panel_url -%}
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index 659fd026bb8..17ceccfd218 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -262,7 +262,6 @@ class HomeAssistantWSGI(object):
resource = CachingStaticResource
else:
resource = web.StaticResource
-
self.app.router.register_resource(resource(url_path, path))
return
diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py
index c2576358f59..c9b094e3f2e 100644
--- a/homeassistant/components/http/static.py
+++ b/homeassistant/components/http/static.py
@@ -65,7 +65,8 @@ class CachingFileResponse(FileResponse):
@asyncio.coroutine
def staticresource_middleware(request, handler):
"""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)
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
index e9f567c04d3..a1fb0ca9cac 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -202,15 +202,16 @@ class WebsocketAPIView(HomeAssistantView):
def get(self, request):
"""Handle an incoming websocket connection."""
# pylint: disable=no-self-use
- return ActiveConnection(request.app['hass']).handle(request)
+ return ActiveConnection(request.app['hass'], request).handle()
class ActiveConnection:
"""Handle an active websocket client connection."""
- def __init__(self, hass):
+ def __init__(self, hass, request):
"""Initialize an active connection."""
self.hass = hass
+ self.request = request
self.wsock = None
self.event_listeners = {}
self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop)
@@ -259,8 +260,9 @@ class ActiveConnection:
self._writer_task.cancel()
@asyncio.coroutine
- def handle(self, request):
+ def handle(self):
"""Handle the websocket connection."""
+ request = self.request
wsock = self.wsock = web.WebSocketResponse()
yield from wsock.prepare(request)
self.debug("Connected")
@@ -350,7 +352,7 @@ class ActiveConnection:
if wsock.closed:
self.debug("Connection closed by client")
else:
- self.log_error("Unexpected TypeError", msg)
+ _LOGGER.exception("Unexpected TypeError: %s", msg)
except ValueError as err:
msg = "Received invalid JSON"
@@ -483,9 +485,14 @@ class ActiveConnection:
Async friendly.
"""
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(
- msg['id'], self.hass.data[frontend.DATA_PANELS]))
+ msg['id'], panels))
def handle_ping(self, msg):
"""Handle ping command.
diff --git a/requirements_all.txt b/requirements_all.txt
index 9b7bb4b3cde..782e9930daa 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -330,7 +330,7 @@ hipnotify==1.0.8
holidays==0.8.1
# homeassistant.components.frontend
-home-assistant-frontend==20171106.0
+home-assistant-frontend==20171110.0
# homeassistant.components.camera.onvif
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9f45cc4516e..083c2792db2 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -74,7 +74,7 @@ hbmqtt==0.8
holidays==0.8.1
# homeassistant.components.frontend
-home-assistant-frontend==20171106.0
+home-assistant-frontend==20171110.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py
index 1b034cfe940..3d8d2b62a2b 100644
--- a/tests/components/test_frontend.py
+++ b/tests/components/test_frontend.py
@@ -52,7 +52,7 @@ def test_frontend_and_static(mock_http_client):
# Test we can retrieve frontend.js
frontendjs = re.search(
- r'(?P\/static\/frontend-[A-Za-z0-9]{32}.html)', text)
+ r'(?P\/frontend_es5\/frontend-[A-Za-z0-9]{32}.html)', text)
assert frontendjs is not None
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
def test_dont_cache_service_worker(mock_http_client):
"""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')
assert resp.status == 200
assert 'cache-control' not in resp.headers
diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py
index 00c824418be..9a56479c469 100644
--- a/tests/components/test_panel_iframe.py
+++ b/tests/components/test_panel_iframe.py
@@ -33,7 +33,7 @@ class TestPanelIframe(unittest.TestCase):
'panel_iframe': conf
})
- @patch.dict('hass_frontend.FINGERPRINTS',
+ @patch.dict('hass_frontend_es5.FINGERPRINTS',
{'panels/ha-panel-iframe.html': 'md5md5'})
def test_correct_config(self):
"""Test correct config."""
@@ -55,20 +55,20 @@ class TestPanelIframe(unittest.TestCase):
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',
'config': {'url': 'http://192.168.1.1'},
'icon': 'mdi:network-wireless',
'title': 'Router',
- 'url': '/static/panels/ha-panel-iframe-md5md5.html',
+ 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'router'
}
- assert panels.get('weather').as_dict() == {
+ assert panels.get('weather').to_response(self.hass, None) == {
'component_name': 'iframe',
'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'},
'icon': 'mdi:weather',
'title': 'Weather',
- 'url': '/static/panels/ha-panel-iframe-md5md5.html',
+ 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'weather',
}
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
index c310b0d5445..8b6c7494214 100644
--- a/tests/components/test_websocket_api.py
+++ b/tests/components/test_websocket_api.py
@@ -290,7 +290,7 @@ def test_get_panels(hass, websocket_client):
"""Test get_panels command."""
yield from hass.components.frontend.async_register_built_in_panel(
'map', 'Map', 'mdi:account-location')
-
+ hass.data[frontend.DATA_JS_VERSION] = 'es5'
websocket_client.send_json({
'id': 5,
'type': wapi.TYPE_GET_PANELS,
@@ -300,8 +300,14 @@ def test_get_panels(hass, websocket_client):
assert msg['id'] == 5
assert msg['type'] == wapi.TYPE_RESULT
assert msg['success']
- assert msg['result'] == {url: panel.as_dict() for url, panel
- in hass.data[frontend.DATA_PANELS].items()}
+ assert msg['result'] == {'map': {
+ 'component_name': 'map',
+ 'url_path': 'map',
+ 'config': None,
+ 'url': None,
+ 'icon': 'mdi:account-location',
+ 'title': 'Map',
+ }}
@asyncio.coroutine