Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 38 additions & 158 deletions homeassistant/components/frontend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
https://home-assistant.io/components/frontend/
"""
import asyncio
import hashlib
import json
import logging
import os
Expand All @@ -30,8 +29,6 @@
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'

CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
Expand Down Expand Up @@ -101,7 +98,7 @@
})


class AbstractPanel:
class Panel:
"""Abstract class for panels."""

# Name of the webcomponent
Expand All @@ -113,30 +110,20 @@ class AbstractPanel:
# Title to show in the sidebar (optional)
sidebar_title = 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

# Config to pass to the webcomponent
config = None

@asyncio.coroutine
def async_register(self, hass):
"""Register panel with HASS."""
panels = hass.data.get(DATA_PANELS)
if panels is None:
panels = hass.data[DATA_PANELS] = {}

if self.frontend_url_path in panels:
_LOGGER.warning("Overwriting component %s", self.frontend_url_path)

if DATA_FINALIZE_PANEL in hass.data:
yield from hass.data[DATA_FINALIZE_PANEL](self)

panels[self.frontend_url_path] = self
def __init__(self, component_name, sidebar_title, sidebar_icon,
frontend_url_path, config):
"""Initialize a built-in panel."""
self.component_name = component_name
self.sidebar_title = sidebar_title
self.sidebar_icon = sidebar_icon
self.frontend_url_path = frontend_url_path or component_name
self.config = config

@callback
def async_register_index_routes(self, router, index_view):
Expand All @@ -147,19 +134,7 @@ def async_register_index_routes(self, router, index_view):
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get)


class BuiltInPanel(AbstractPanel):
"""Panel that is part of hass_frontend."""

def __init__(self, component_name, sidebar_title, sidebar_icon,
frontend_url_path, config):
"""Initialize a built-in panel."""
self.component_name = component_name
self.sidebar_title = sidebar_title
self.sidebar_icon = sidebar_icon
self.frontend_url_path = frontend_url_path or component_name
self.config = config

@callback
def to_response(self, hass, request):
"""Panel as dictionary."""
return {
Expand All @@ -171,95 +146,25 @@ def to_response(self, hass, request):
}


class ExternalPanel(AbstractPanel):
"""Panel that is added by a custom component."""

REGISTERED_COMPONENTS = set()

def __init__(self, component_name, path, md5, sidebar_title, sidebar_icon,
frontend_url_path, config):
"""Initialize an external panel."""
self.component_name = component_name
self.path = path
self.md5 = md5
self.sidebar_title = sidebar_title
self.sidebar_icon = sidebar_icon
self.frontend_url_path = frontend_url_path or component_name
self.config = config

@asyncio.coroutine
def async_finalize(self, hass, frontend_repository_path):
"""Finalize this panel for usage.

frontend_repository_path is set, will be prepended to path of built-in
components.
"""
try:
if self.md5 is None:
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_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_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)

def to_response(self, hass, request):
"""Panel as dictionary."""
result = {
'component_name': self.component_name,
'icon': self.sidebar_icon,
'title': self.sidebar_title,
'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


@bind_hass
@asyncio.coroutine
def async_register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, frontend_url_path=None,
config=None):
async def async_register_built_in_panel(hass, component_name,
sidebar_title=None, sidebar_icon=None,
frontend_url_path=None, config=None):
"""Register a built-in panel."""
panel = BuiltInPanel(component_name, sidebar_title, sidebar_icon,
frontend_url_path, config)
yield from panel.async_register(hass)
panel = Panel(component_name, sidebar_title, sidebar_icon,
frontend_url_path, config)

panels = hass.data.get(DATA_PANELS)
if panels is None:
panels = hass.data[DATA_PANELS] = {}

@bind_hass
@asyncio.coroutine
def async_register_panel(hass, component_name, path, md5=None,
sidebar_title=None, sidebar_icon=None,
frontend_url_path=None, config=None):
"""Register a panel for the frontend.

component_name: name of the web component
path: path to the HTML of the web component
(required unless url is provided)
md5: the md5 hash of the web component (for versioning in URL, optional)
sidebar_title: title to show in the sidebar (optional)
sidebar_icon: icon to show next to title in sidebar (optional)
url_path: name to use in the URL (defaults to component_name)
config: config to be passed into the web component
"""
panel = ExternalPanel(component_name, path, md5, sidebar_title,
sidebar_icon, frontend_url_path, config)
yield from panel.async_register(hass)
if panel.frontend_url_path in panels:
_LOGGER.warning("Overwriting component %s", panel.frontend_url_path)

if DATA_FINALIZE_PANEL in hass.data:
hass.data[DATA_FINALIZE_PANEL](panel)

panels[panel.frontend_url_path] = panel


@bind_hass
Expand All @@ -278,11 +183,10 @@ def add_manifest_json_key(key, val):
MANIFEST_JSON[key] = val


@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the serving of the frontend."""
if list(hass.auth.async_auth_providers):
client = yield from hass.auth.async_create_client(
client = await hass.auth.async_create_client(
'Home Assistant Frontend',
redirect_uris=['/'],
no_secret=True,
Expand Down Expand Up @@ -331,24 +235,22 @@ def async_setup(hass, config):
index_view = IndexView(repo_path, js_version, client)
hass.http.register_view(index_view)

async def finalize_panel(panel):
@callback
def async_finalize_panel(panel):
"""Finalize setup of a panel."""
if hasattr(panel, 'async_finalize'):
await panel.async_finalize(hass, repo_path)
panel.async_register_index_routes(hass.http.app.router, index_view)

yield from asyncio.wait([
await asyncio.wait([
async_register_built_in_panel(hass, panel)
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop)

hass.data[DATA_FINALIZE_PANEL] = finalize_panel
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel

# Finalize registration of panels that registered before frontend was setup
# This includes the built-in panels from line above.
yield from asyncio.wait(
[finalize_panel(panel) for panel in hass.data[DATA_PANELS].values()],
loop=hass.loop)
for panel in hass.data[DATA_PANELS].values():
async_finalize_panel(panel)

if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
Expand Down Expand Up @@ -456,38 +358,23 @@ def get_template(self, latest):

return tpl

@asyncio.coroutine
def get(self, request, extra=None):
async def get(self, request, extra=None):
"""Serve the index view."""
hass = request.app['hass']
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)

if request.path == '/':
panel = 'states'
else:
panel = request.path.split('/')[1]

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_es5

no_auth = '1'
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'

template = yield from hass.async_add_job(self.get_template, latest)
template = await hass.async_add_job(self.get_template, latest)

extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5

template_params = dict(
no_auth=no_auth,
panel_url=panel_url,
panels=hass.data[DATA_PANELS],
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[extra_key],
)
Expand All @@ -506,7 +393,7 @@ class ManifestJSONView(HomeAssistantView):
url = '/manifest.json'
name = 'manifestjson'

@asyncio.coroutine
@callback
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True)
Expand Down Expand Up @@ -537,23 +424,16 @@ class TranslationsView(HomeAssistantView):
url = '/api/translations/{language}'
name = 'api:translations'

@asyncio.coroutine
def get(self, request, language):
async def get(self, request, language):
"""Return translations."""
hass = request.app['hass']

resources = yield from async_get_translations(hass, language)
resources = await async_get_translations(hass, language)
return self.json({
'resources': resources,
})


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.
Expand Down
Binary file not shown.
11 changes: 1 addition & 10 deletions tests/components/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from homeassistant.setup import async_setup_component
from homeassistant.components.frontend import (
DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL,
CONF_EXTRA_HTML_URL_ES5, DATA_PANELS)
CONF_EXTRA_HTML_URL_ES5)
from homeassistant.components import websocket_api as wapi


Expand Down Expand Up @@ -183,15 +183,6 @@ def test_extra_urls_es5(mock_http_client_with_urls):
assert text.find('href="https://domain.com/my_extra_url_es5.html"') >= 0


@asyncio.coroutine
def test_panel_without_path(hass):
"""Test panel registration without file path."""
yield from hass.components.frontend.async_register_panel(
'test_component', 'nonexistant_file')
yield from async_setup_component(hass, 'frontend', {})
assert 'test_component' not in hass.data[DATA_PANELS]


async def test_get_panels(hass, hass_ws_client):
"""Test get_panels command."""
await async_setup_component(hass, 'frontend')
Expand Down