Add support for dynamic frontend panels

This commit is contained in:
Paulus Schoutsen 2016-07-16 22:32:25 -07:00
parent 35a57e1385
commit 22b4aebeb3
21 changed files with 197 additions and 119 deletions

View file

@ -1,37 +1,112 @@
"""Handle the frontend for Home Assistant."""
import logging
import os
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version
from .version import FINGERPRINTS
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
PANELS = {}
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
_LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, title=None, icon=None,
url_name=None, config=None):
"""Register a built-in panel."""
path = 'panels/ha-panel-{}.html'.format(component_name)
register_panel(hass, component_name, os.path.join(STATIC_PATH, path),
FINGERPRINTS[path], title, icon, url_name, config)
def register_panel(hass, component_name, path, md5, title=None, icon=None,
url_name=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
md5: the md5 hash of the web component (for versioning)
title: title to show in the sidebar (optional)
icon: icon to show next to title in sidebar (optional)
url_name: name to use in the url (defaults to component_name)
config: config to be passed into the web component
Warning: this API will probably change. Use at own risk.
"""
if url_name is None:
url_name = component_name
if url_name in PANELS:
_LOGGER.warning('Overwriting component %s', url_name)
if not os.path.isfile(path):
_LOGGER.warning('Panel %s component does not exist: %s',
component_name, path)
data = {
'url_name': url_name,
'component_name': component_name,
}
if title:
data['title'] = title
if icon:
data['icon'] = icon
if config is not None:
data['config'] = config
if hass.wsgi.development:
data['url'] = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name))
else:
url = URL_PANEL_COMPONENT.format(component_name)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
hass.wsgi.register_static_path(url, path)
data['url'] = fprinted_url
PANELS[url_name] = data
# TODO register /<component_name> to index view.
def setup(hass, config):
"""Setup serving the frontend."""
hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView)
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
hass.wsgi.register_static_path(
"/service_worker.js",
os.path.join(www_static_path, sw_path),
0
)
hass.wsgi.register_static_path(
"/robots.txt",
os.path.join(www_static_path, "robots.txt")
)
hass.wsgi.register_static_path("/static", www_static_path)
hass.wsgi.register_static_path("/service_worker.js",
os.path.join(STATIC_PATH, sw_path), 0)
hass.wsgi.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt"))
hass.wsgi.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template'):
register_built_in_panel(hass, panel)
def register_frontend_index(event):
"""Register the frontend index urls.
Done when Home Assistant is started so that all panels are known.
"""
hass.wsgi.register_view(IndexView(
hass, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
return True
@ -48,6 +123,7 @@ class BootstrapView(HomeAssistantView):
'states': self.hass.states.all(),
'events': api.events_json(self.hass),
'services': api.services_json(self.hass),
'panels': PANELS,
})
@ -57,16 +133,15 @@ class IndexView(HomeAssistantView):
url = '/'
name = "frontend:index"
requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/devTemplate',
'/states', '/states/<entity:entity_id>']
extra_urls = ['/states', '/states/<entity:entity_id>']
def __init__(self, hass):
def __init__(self, hass, extra_urls):
"""Initialize the frontend view."""
super().__init__(hass)
from jinja2 import FileSystemLoader, Environment
self.extra_urls = self.extra_urls + extra_urls
self.templates = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/')
@ -76,32 +151,24 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
map_url = ('/static/home-assistant-polymer/src/layouts/'
'partial-map.html')
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
'dev-tools.html')
else:
core_url = '/static/core-{}.js'.format(version.CORE)
ui_url = '/static/frontend-{}.html'.format(version.UI)
map_url = '/static/partial-map-{}.html'.format(version.MAP)
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
core_url = '/static/core-{}.js'.format(
FINGERPRINTS['core.js'])
ui_url = '/static/frontend-{}.html'.format(
FINGERPRINTS['frontend.html'])
# auto login if no password was set
if self.hass.config.api.api_password is None:
auth = 'true'
else:
auth = 'false'
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
no_auth = 'false' if self.hass.config.api.api_password else 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = self.templates.get_template('index.html')
# pylint is wrong
# pylint: disable=no-member
resp = template.render(
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'])
return self.Response(resp, mimetype='text/html')

View file

@ -1,2 +0,0 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "758957b7ea989d6beca60e218ea7f7dd"

View file

@ -5,14 +5,14 @@
<title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json'>
<link rel='icon' href='/static/favicon.ico'>
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'>
href='/static/icons/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
@ -65,16 +65,12 @@
.getElementById('ha-init-skeleton')
.classList.add('error');
};
window.noAuth = {{ auth }};
window.deferredLoading = {
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
window.noAuth = {{ no_auth }};
</script>
</head>
<body fullbleed>
<div id='ha-init-skeleton'>
<img src='/static/favicon-192x192.png' height='192'>
<img src='/static/icons/favicon-192x192.png' height='192'>
<paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div>

View file

@ -1,5 +1,24 @@
<<<<<<< HEAD
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f"
MAP = "c922306de24140afd14f857f927bf8f0"
DEV = "b7079ac3121b95b9856e5603a6d8a263"
=======
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = {
"core.js": "4783ccdb2f15d3a63fcab9be411629b7",
"frontend.html": "6c50bcdd8c8b7d840bc2cdef02e9ee39",
"mdi.html": "a7fa9237b7da93951076b4fe26cb8cd2",
"panels/ha-panel-dev-event.html": "f1f47bf3f0e305f855a99dd1ee788045",
"panels/ha-panel-dev-info.html": "50a7817f60675feef3e4c9aa9a043fe1",
"panels/ha-panel-dev-service.html": "d507e0018faf73d58a1fdeb2a0368505",
"panels/ha-panel-dev-state.html": "6a4418826419f235fd9fcc5e952e858c",
"panels/ha-panel-dev-template.html": "cc8917fdad5a4fc81cc1d4104ea0d2dc",
"panels/ha-panel-history.html": "999ecb591df76d6a4aba1fe84e04baf1",
"panels/ha-panel-iframe.html": "f4aaaf31321cd8bfb57755c24af7fc31",
"panels/ha-panel-logbook.html": "6dde7050246875774ec9fce60df05442",
"panels/ha-panel-map.html": "d2cf412d52f43431307bbc2e216be9c9"
}
>>>>>>> Add support for dynamic frontend panels

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before After
Before After

View file

@ -7,22 +7,22 @@
"background_color": "#FFFFFF",
"icons": [
{
"src": "/static/favicon-192x192.png",
"src": "/static/icons/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/favicon-384x384.png",
"src": "/static/icons/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/favicon-512x512.png",
"src": "/static/icons/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/static/favicon-1024x1024.png",
"src": "/static/icons/favicon-1024x1024.png",
"sizes": "1024x1024",
"type": "image/png"
}

View file

@ -11,6 +11,7 @@ from itertools import groupby
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history'
@ -153,6 +154,7 @@ def setup(hass, config):
"""Setup the history hooks."""
hass.wsgi.register_view(Last5StatesView)
hass.wsgi.register_view(HistoryPeriodView)
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True

View file

@ -14,6 +14,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
@ -24,7 +25,7 @@ from homeassistant.helpers import template
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
DEPENDENCIES = ['recorder', 'frontend']
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
@ -75,6 +76,9 @@ def setup(hass, config):
hass.wsgi.register_view(LogbookView)
register_built_in_panel(hass, 'logbook', 'Logbook',
'mdi:format-list-bulleted-type')
hass.services.register(DOMAIN, 'log', log_message,
schema=LOG_MESSAGE_SCHEMA)
return True

View file

@ -2,36 +2,21 @@
cd "$(dirname "$0")/.."
cd homeassistant/components/frontend/www_static/home-assistant-polymer
cd homeassistant/components/frontend/www_static
rm -rf core.js* frontend.html* webcomponents-lite.min.js* panels
cd home-assistant-polymer
npm run clean
npm run frontend_prod
cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
cp build/frontend.html ..
gzip build/frontend.html -c -k -9 > ../frontend.html.gz
cp build/partial-map.html ..
gzip build/partial-map.html -c -k -9 > ../partial-map.html.gz
cp build/dev-tools.html ..
gzip build/dev-tools.html -c -k -9 > ../dev-tools.html.gz
cp build/_core_compiled.js ../core.js
gzip build/_core_compiled.js -c -k -9 > ../core.js.gz
cp -r build/* ..
node script/sw-precache.js
cp build/service_worker.js ..
gzip build/service_worker.js -c -k -9 > ../service_worker.js.gz
cd ..
gzip -f -k -9 *.html *.js ./panels/*.html
# Generate the MD5 hash of the new frontend
cd ../..
echo '"""DO NOT MODIFY. Auto-generated by build_frontend script."""' > version.py
if [ $(command -v md5) ]; then
echo 'CORE = "'`md5 -q www_static/core.js`'"' >> version.py
echo 'UI = "'`md5 -q www_static/frontend.html`'"' >> version.py
echo 'MAP = "'`md5 -q www_static/partial-map.html`'"' >> version.py
echo 'DEV = "'`md5 -q www_static/dev-tools.html`'"' >> version.py
elif [ $(command -v md5sum) ]; then
echo 'CORE = "'`md5sum www_static/core.js | cut -c-32`'"' >> version.py
echo 'UI = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> version.py
echo 'MAP = "'`md5sum www_static/partial-map.html | cut -c-32`'"' >> version.py
echo 'DEV = "'`md5sum www_static/dev-tools.html | cut -c-32`'"' >> version.py
else
echo 'Could not find an MD5 utility'
fi
cd ../../../..
script/fingerprint_frontend.py

37
script/fingerprint_frontend.py Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python3
from collections import OrderedDict
import glob
import hashlib
import json
fingerprint_file = 'homeassistant/components/frontend/version.py'
base_dir = 'homeassistant/components/frontend/www_static/'
def fingerprint():
"""Fingerprint the frontend files."""
files = (glob.glob(base_dir + '**/*.html') +
glob.glob(base_dir + '*.html') +
glob.glob(base_dir + 'core.js'))
md5s = OrderedDict()
for fil in sorted(files):
name = fil[len(base_dir):]
with open(fil) as fp:
md5 = hashlib.md5(fp.read().encode('utf-8')).hexdigest()
md5s[name] = md5
template = """\"\"\"DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.\"\"\"
FINGERPRINTS = {}
"""
result = template.format(json.dumps(md5s, indent=4))
with open(fingerprint_file, 'w') as fp:
fp.write(result)
if __name__ == '__main__':
fingerprint()

View file

@ -1,38 +1,24 @@
#!/usr/bin/env python3
"""Download the latest Polymer v1 iconset for materialdesignicons.com."""
import hashlib
import gzip
import os
import re
import requests
import sys
from fingerprint_frontend import fingerprint
GETTING_STARTED_URL = ('https://raw.githubusercontent.com/Templarian/'
'MaterialDesign/master/site/getting-started.savvy')
DOWNLOAD_LINK = re.compile(r'(/api/download/polymer/v1/([A-Z0-9-]{36}))')
START_ICONSET = '<iron-iconset-svg'
CUR_VERSION = re.compile(r'VERSION = "([A-Za-z0-9]{32})"')
OUTPUT_BASE = os.path.join('homeassistant', 'components', 'frontend')
VERSION_OUTPUT = os.path.join(OUTPUT_BASE, 'mdi_version.py')
ICONSET_OUTPUT = os.path.join(OUTPUT_BASE, 'www_static', 'mdi.html')
ICONSET_OUTPUT_GZ = os.path.join(OUTPUT_BASE, 'www_static', 'mdi.html.gz')
def get_local_version():
"""Parse the local version."""
try:
with open(VERSION_OUTPUT) as inp:
for line in inp:
match = CUR_VERSION.search(line)
if match:
return match.group(1)
except FileNotFoundError:
return False
return False
def get_remote_version():
"""Get current version and download link."""
gs_page = requests.get(GETTING_STARTED_URL).text
@ -43,10 +29,7 @@ def get_remote_version():
print("Unable to find download link")
sys.exit()
url = 'https://materialdesignicons.com' + mdi_download.group(1)
version = mdi_download.group(2).replace('-', '')
return version, url
return 'https://materialdesignicons.com' + mdi_download.group(1)
def clean_component(source):
@ -54,7 +37,7 @@ def clean_component(source):
return source[source.index(START_ICONSET):]
def write_component(version, source):
def write_component(source):
"""Write component."""
with open(ICONSET_OUTPUT, 'w') as outp:
print('Writing icons to', ICONSET_OUTPUT)
@ -64,12 +47,6 @@ def write_component(version, source):
print('Writing icons gz to', ICONSET_OUTPUT_GZ)
outp.write(source.encode('utf-8'))
with open(VERSION_OUTPUT, 'w') as outp:
print('Generating version file', VERSION_OUTPUT)
outp.write(
'"""DO NOT MODIFY. Auto-generated by update_mdi script."""\n')
outp.write('VERSION = "{}"\n'.format(version))
def main():
"""Main section of the script."""
@ -79,19 +56,11 @@ def main():
print("materialdesignicons.com icon updater")
local_version = get_local_version()
# The remote version is not reliable.
_, remote_url = get_remote_version()
remote_url = get_remote_version()
source = clean_component(requests.get(remote_url).text)
new_version = hashlib.md5(source.encode('utf-8')).hexdigest()
write_component(source)
fingerprint()
if local_version == new_version:
print('Already on the latest version.')
sys.exit()
write_component(new_version, source)
print('Updated to latest version')
if __name__ == '__main__':

View file

@ -7,7 +7,7 @@ import unittest
import requests
import homeassistant.bootstrap as bootstrap
import homeassistant.components.http as http
from homeassistant.components import frontend, http
from homeassistant.const import HTTP_HEADER_HA_AUTH
from tests.common import get_test_instance_port, get_test_home_assistant
@ -48,6 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name
def tearDownModule(): # pylint: disable=invalid-name
"""Stop everything that was started."""
hass.stop()
frontend.PANELS = {}
class TestFrontend(unittest.TestCase):