Merge pull request #2537 from home-assistant/frontend-panels

Frontend panels
This commit is contained in:
Paulus Schoutsen
2016-07-16 23:54:12 -07:00
committed by GitHub
56 changed files with 335 additions and 401 deletions

View File

@@ -13,7 +13,8 @@ ATTR_URL = 'url'
ATTR_URL_DEFAULT = 'https://www.google.com' ATTR_URL_DEFAULT = 'https://www.google.com'
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url, # pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(),
}) })

View File

@@ -24,7 +24,8 @@ ATTR_URL = "url"
ATTR_SUBDIR = "subdir" ATTR_SUBDIR = "subdir"
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): vol.Url, # pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL): vol.Url(),
vol.Optional(ATTR_SUBDIR): cv.string, vol.Optional(ATTR_SUBDIR): cv.string,
}) })

View File

@@ -1,37 +1,111 @@
"""Handle the frontend for Home Assistant.""" """Handle the frontend for Home Assistant."""
import logging
import os import os
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version from .version import FINGERPRINTS
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api'] 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."""
# pylint: disable=too-many-arguments
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.
"""
# pylint: disable=too-many-arguments
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
def setup(hass, config): def setup(hass, config):
"""Setup serving the frontend.""" """Setup serving the frontend."""
hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView) hass.wsgi.register_view(BootstrapView)
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
if hass.wsgi.development: if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js" sw_path = "home-assistant-polymer/build/service_worker.js"
else: else:
sw_path = "service_worker.js" sw_path = "service_worker.js"
hass.wsgi.register_static_path( hass.wsgi.register_static_path("/service_worker.js",
"/service_worker.js", os.path.join(STATIC_PATH, sw_path), 0)
os.path.join(www_static_path, sw_path), hass.wsgi.register_static_path("/robots.txt",
0 os.path.join(STATIC_PATH, "robots.txt"))
) hass.wsgi.register_static_path("/static", STATIC_PATH)
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("/local", hass.config.path('www')) 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 return True
@@ -48,6 +122,7 @@ class BootstrapView(HomeAssistantView):
'states': self.hass.states.all(), 'states': self.hass.states.all(),
'events': api.events_json(self.hass), 'events': api.events_json(self.hass),
'services': api.services_json(self.hass), 'services': api.services_json(self.hass),
'panels': PANELS,
}) })
@@ -57,16 +132,15 @@ class IndexView(HomeAssistantView):
url = '/' url = '/'
name = "frontend:index" name = "frontend:index"
requires_auth = False requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState', extra_urls = ['/states', '/states/<entity:entity_id>']
'/devEvent', '/devInfo', '/devTemplate',
'/states', '/states/<entity:entity_id>']
def __init__(self, hass): def __init__(self, hass, extra_urls):
"""Initialize the frontend view.""" """Initialize the frontend view."""
super().__init__(hass) super().__init__(hass)
from jinja2 import FileSystemLoader, Environment from jinja2 import FileSystemLoader, Environment
self.extra_urls = self.extra_urls + extra_urls
self.templates = Environment( self.templates = Environment(
loader=FileSystemLoader( loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/') os.path.join(os.path.dirname(__file__), 'templates/')
@@ -76,32 +150,24 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None): def get(self, request, entity_id=None):
"""Serve the index view.""" """Serve the index view."""
if self.hass.wsgi.development: 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' 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: else:
core_url = '/static/core-{}.js'.format(version.CORE) core_url = '/static/core-{}.js'.format(
ui_url = '/static/frontend-{}.html'.format(version.UI) FINGERPRINTS['core.js'])
map_url = '/static/partial-map-{}.html'.format(version.MAP) ui_url = '/static/frontend-{}.html'.format(
dev_url = '/static/dev-tools-{}.html'.format(version.DEV) FINGERPRINTS['frontend.html'])
# auto login if no password was set # auto login if no password was set
if self.hass.config.api.api_password is None: no_auth = 'false' if self.hass.config.api.api_password else 'true'
auth = 'true'
else:
auth = 'false'
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = self.templates.get_template('index.html') template = self.templates.get_template('index.html')
# pylint is wrong # pylint is wrong
# pylint: disable=no-member # pylint: disable=no-member
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth, core_url=core_url, ui_url=ui_url, no_auth=no_auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION) icons_url=icons_url, icons=FINGERPRINTS['mdi.html'])
return self.Response(resp, mimetype='text/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> <title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json'> <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' <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='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/> <meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/> <meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/> <meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/> <meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/> <meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'> <meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'> <meta name='viewport' content='width=device-width, user-scalable=no'>
@@ -65,16 +65,12 @@
.getElementById('ha-init-skeleton') .getElementById('ha-init-skeleton')
.classList.add('error'); .classList.add('error');
}; };
window.noAuth = {{ auth }}; window.noAuth = {{ no_auth }};
window.deferredLoading = {
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
</script> </script>
</head> </head>
<body fullbleed> <body fullbleed>
<div id='ha-init-skeleton'> <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> <paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a> Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div> </div>

View File

@@ -1,5 +1,17 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f" FINGERPRINTS = {
MAP = "c922306de24140afd14f857f927bf8f0" "core.js": "4783ccdb2f15d3a63fcab9be411629b7",
DEV = "b7079ac3121b95b9856e5603a6d8a263" "dev-tools.html": "b7079ac3121b95b9856e5603a6d8a263",
"frontend.html": "35a686ea968959f7e09c7d628c51a823",
"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": "efa8d0f33475b077d9b2bcc6a56aef05",
"panels/ha-panel-logbook.html": "6dde7050246875774ec9fce60df05442",
"panels/ha-panel-map.html": "d2cf412d52f43431307bbc2e216be9c9"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><dom-module id="events-list" assetpath="/"><style>ul{margin:0;padding:0}li{list-style:none;line-height:2em}a{color:var(--dark-primary-color)}</style><template><ul><template is="dom-repeat" items="[[events]]" as="event"><li><a href="#" on-click="eventSelected">{{event.event}}</a> <span>(</span><span>{{event.listenerCount}}</span><span> listeners)</span></li></template></ul></template></dom-module><script>Polymer({is:"events-list",behaviors:[window.hassBehavior],properties:{hass:{type:Object},events:{type:Array,bindNuclear:function(e){return[e.eventGetters.entityMap,function(e){return e.valueSeq().sortBy(function(e){return e.event}).toArray()}]}}},eventSelected:function(e){e.preventDefault(),this.fire("event-selected",{eventType:e.model.event.event})}})</script></div><dom-module id="ha-panel-dev-event"><style is="custom-style" include="iron-flex iron-positioning"></style><style>.content{@apply(--paper-font-body1);margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.ha-form{margin-right:16px}.header{@apply(--paper-font-title)}</style><template><partial-base narrow="{{narrow}}" show-menu="[[showMenu]]"><span header-title="">Events</span><div class$="[[computeFormClasses(narrow)]]"><div class="flex"><p>Fire an event on the event bus.</p><div class="ha-form"><paper-input label="Event Type" autofocus="" required="" value="{{eventType}}"></paper-input><paper-textarea label="Event Data (JSON, optional)" value="{{eventData}}"></paper-textarea><paper-button on-tap="fireEvent" raised="">Fire Event</paper-button></div></div><div><div class="header">Available Events</div><events-list on-event-selected="eventSelected" hass="[[hass]]"></events-list></div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-event",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(e){this.eventType=e.detail.eventType},fireEvent:function(){var e;try{e=this.eventData?JSON.parse(this.eventData):{}}catch(e){return void alert("Error parsing JSON: "+e)}this.hass.eventActions.fireEvent(this.eventType,e)},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"}})</script></body></html>

View File

@@ -0,0 +1,2 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><style is="custom-style" include="iron-positioning"></style><style>.content{margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
clear: both;white-space:pre-wrap}</style><template><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">About</span><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a><a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>

View File

@@ -0,0 +1 @@
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><dom-module id="services-list" assetpath="/"><style>ul{margin:0;padding:0}li{list-style:none;line-height:2em}a{color:var(--dark-primary-color)}</style><template><ul><template is="dom-repeat" items="[[computeDomains(serviceDomains)]]" as="domain"><template is="dom-repeat" items="[[computeServices(serviceDomains, domain)]]" as="service"><li><a href="#" on-click="serviceClicked"><span>[[domain]]</span>/<span>[[service]]</span></a></li></template></template></ul></template></dom-module><script>Polymer({is:"services-list",behaviors:[window.hassBehavior],properties:{hass:{type:Object},serviceDomains:{type:Array,bindNuclear:function(e){return e.serviceGetters.entityMap}}},computeDomains:function(e){return e.valueSeq().map(function(e){return e.domain}).sort().toJS()},computeServices:function(e,r){return e.get(r).get("services").keySeq().toArray()},serviceClicked:function(e){e.preventDefault(),this.fire("service-selected",{domain:e.model.domain,service:e.model.service})}})</script></div><dom-module id="ha-panel-dev-service"><style is="custom-style" include="iron-flex iron-positioning"></style><style>.content{@apply(--paper-font-body1);margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.ha-form{margin-right:16px}.description{margin-top:24px;white-space:pre-wrap}.header{@apply(--paper-font-title)}</style><template><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">Services</span><div class$="[[computeFormClasses(narrow)]]"><div class="flex"><p>Call a service from a component.</p><div class="ha-form"><paper-input label="Domain" autofocus="" value="{{domain}}"></paper-input><paper-input label="Service" value="{{service}}"></paper-input><paper-textarea label="Service Data (JSON, optional)" value="{{serviceData}}"></paper-textarea><paper-button on-tap="callService" raised="">Call Service</paper-button></div><div class="description">[[description]]</div></div><div><div class="header">Available services</div><services-list on-service-selected="serviceSelected" hass="[[hass]]"></services-list></div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-service",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},domain:{type:String,value:""},service:{type:String,value:""},serviceData:{type:String,value:""},description:{type:String,computed:"computeDescription(hass, domain, service)"}},computeDescription:function(e,t,i){return e.reactor.evaluate([e.serviceGetters.entityMap,function(e){return e.has(t)&&e.get(t).get("services").has(i)?JSON.stringify(e.get(t).get("services").get(i).toJS(),null,2):"No description available"}])},serviceSelected:function(e){this.domain=e.detail.domain,this.service=e.detail.service},callService:function(){var e;try{e=this.serviceData?JSON.parse(this.serviceData):{}}catch(e){return void alert("Error parsing JSON: "+e)}this.hass.serviceActions.callService(this.domain,this.service,e)},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"}})</script></body></html>

View File

@@ -0,0 +1 @@
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><dom-module id="entity-list" assetpath="/"><style>ul{margin:0;padding:0}li{list-style:none;line-height:2em}a{color:var(--dark-primary-color)}</style><template><ul><template is="dom-repeat" items="[[entities]]" as="entity"><li><a href="#" on-click="entitySelected">[[entity.entityId]]</a></li></template></ul></template></dom-module><script>Polymer({is:"entity-list",behaviors:[window.hassBehavior],properties:{hass:{type:Object},entities:{type:Array,bindNuclear:function(t){return[t.entityGetters.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.entityId}).toArray()}]}}},entitySelected:function(t){t.preventDefault(),this.fire("entity-selected",{entityId:t.model.entity.entityId})}})</script></div><dom-module id="ha-panel-dev-state"><style is="custom-style" include="iron-flex iron-positioning"></style><style>.content{@apply(--paper-font-body1);margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.ha-form{margin-right:16px}.header{@apply(--paper-font-title)}</style><template><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">States</span><div class$="[[computeFormClasses(narrow)]]"><div class="flex"><p>Set the representation of a device within Home Assistant.<br>This will not communicate with the actual device.</p><div class="ha-form"><paper-input label="Entity ID" autofocus="" required="" value="{{entityId}}"></paper-input><paper-input label="State" required="" value="{{state}}"></paper-input><paper-textarea label="State attributes (JSON, optional)" value="{{stateAttributes}}"></paper-textarea><paper-button on-tap="handleSetState" raised="">Set State</paper-button></div></div><div><div class="header">Current entities</div><entity-list on-entity-selected="entitySelected" hass="[[hass]]"></entity-list></div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-state",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(t){var e=t?JSON.stringify(t,null," "):"";this.$.inputData.value=e,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(t){var e=this.hass.reactor.evaluate(this.hass.entityGetters.byId(t.detail.entityId));this.entityId=e.entityId,this.state=e.state,this.stateAttributes=JSON.stringify(e.attributes,null," ")},handleSetState:function(){var t;try{t=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.entityActions.save({entityId:this.entityId,state:this.state,attributes:t})},computeFormClasses:function(t){return t?"content fit":"content fit layout horizontal"}})</script></body></html>

View File

@@ -0,0 +1,2 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-template"><style is="custom-style" include="iron-flex iron-positioning"></style><style>.content{@apply(--paper-font-body1);margin-top:64px;padding:16px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.edit-pane{margin-right:16px}.edit-pane a{color:var(--dark-primary-color)}.horizontal .edit-pane{max-width:50%}.render-pane{position:relative;max-width:50%}.render-spinner{position:absolute;top:8px;right:8px}.rendered{@apply(--paper-font-code1)
clear: both;white-space:pre-wrap}.rendered.error{color:red}</style><template><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">Template Editor</span><div class$="[[computeFormClasses(narrow)]]"><div class="edit-pane"><p>Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.</p><ul><li><a href="http://jinja.pocoo.org/docs/dev/templates/" target="_blank">Jinja2 tempate documentation</a></li><li><a href="https://home-assistant.io/topics/templating/" target="_blank">Home Assistant template extensions</a></li></ul><paper-textarea label="Template" value="{{template}}"></paper-textarea></div><div class="render-pane"><paper-spinner class="render-spinner" active="[[rendering]]"></paper-spinner><pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre></div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-template",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'{%- if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate.bind(this),500)},renderTemplate:function(){this.rendering=!0,this.hass.templateActions.render(this.template).then(function(e){this.processed=e,this.rendering=!1}.bind(this),function(e){this.processed=e.message,this.error=!0,this.rendering=!1}.bind(this))}})</script></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-iframe"><style>iframe{border:0;width:100%;height:100%}</style><template><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">[[panel.title]]</span><iframe src="[[panel.config.url]]" sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"></iframe></partial-base></template></dom-module><script>Polymer({is:"ha-panel-iframe",properties:{panel:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean}}})</script></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -1,258 +1 @@
/** "use strict";function deleteAllCaches(){return caches.keys().then(function(e){return Promise.all(e.map(function(e){return caches.delete(e)}))})}var PrecacheConfig=[["/","3bdc53ee7d627d4512407b623455f138"],["/frontend/panels/dev-event-f1f47bf3f0e305f855a99dd1ee788045.html","2831a46da3a8ffb1339eb4cad24f9623"],["/frontend/panels/dev-info-50a7817f60675feef3e4c9aa9a043fe1.html","92d473c7565c9dd0ee188ed96db7df6c"],["/frontend/panels/dev-service-d507e0018faf73d58a1fdeb2a0368505.html","ee7861775eeba5482b4cbcaece80c893"],["/frontend/panels/dev-state-6a4418826419f235fd9fcc5e952e858c.html","2b908b2429154b12f9550dcf6a227844"],["/frontend/panels/dev-template-cc8917fdad5a4fc81cc1d4104ea0d2dc.html","e00be617ab0d30f29a6267a3288d011d"],["/frontend/panels/map-d2cf412d52f43431307bbc2e216be9c9.html","0adb7e1753edb2c8dd288f7b0ab36eb6"],["/static/core-4783ccdb2f15d3a63fcab9be411629b7.js","c1593821e5fa766c0c9d15009daff8fb"],["/static/frontend-35a686ea968959f7e09c7d628c51a823.html","50e6a71698e1dc75e72f8bbeb71ef7f7"],["/static/mdi-a7fa9237b7da93951076b4fe26cb8cd2.html","bd484adf5c530c651d98621ece280d3a"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],CacheNamePrefix="sw-precache-v1--"+(self.registration?self.registration.scope:"")+"-",IgnoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},getCacheBustedUrl=function(e,t){t=t||Date.now();var a=new URL(e);return a.search+=(a.search?"&":"")+"sw-precache="+t,a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},populateCurrentCacheNames=function(e,t,a){var n={},c={};return e.forEach(function(e){var r=new URL(e[0],a).toString(),o=t+r+"-"+e[1];c[o]=r,n[r]=o}),{absoluteUrlToCacheName:n,currentCacheNamesToAbsoluteUrl:c}},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},mappings=populateCurrentCacheNames(PrecacheConfig,CacheNamePrefix,self.location),AbsoluteUrlToCacheName=mappings.absoluteUrlToCacheName,CurrentCacheNamesToAbsoluteUrl=mappings.currentCacheNamesToAbsoluteUrl;self.addEventListener("install",function(e){e.waitUntil(Promise.all(Object.keys(CurrentCacheNamesToAbsoluteUrl).map(function(e){return caches.open(e).then(function(t){return t.keys().then(function(a){if(0===a.length){var n=e.split("-").pop(),c=getCacheBustedUrl(CurrentCacheNamesToAbsoluteUrl[e],n),r=new Request(c,{credentials:"same-origin"});return fetch(r).then(function(a){return a.ok?t.put(CurrentCacheNamesToAbsoluteUrl[e],a):(console.error("Request for %s returned a response status %d, so not attempting to cache it.",c,a.status),caches.delete(e))})}})})})).then(function(){return caches.keys().then(function(e){return Promise.all(e.filter(function(e){return 0===e.indexOf(CacheNamePrefix)&&!(e in CurrentCacheNamesToAbsoluteUrl)}).map(function(e){return caches.delete(e)}))})}).then(function(){"function"==typeof self.skipWaiting&&self.skipWaiting()}))}),self.clients&&"function"==typeof self.clients.claim&&self.addEventListener("activate",function(e){e.waitUntil(self.clients.claim())}),self.addEventListener("message",function(e){"delete_all"===e.data.command&&(console.log("About to delete all caches..."),deleteAllCaches().then(function(){console.log("Caches deleted."),e.ports[0].postMessage({error:null})}).catch(function(t){console.log("Caches not deleted:",t),e.ports[0].postMessage({error:t})}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t=stripIgnoredUrlParameters(e.request.url,IgnoreUrlParametersMatching),a=AbsoluteUrlToCacheName[t],n="index.html";!a&&n&&(t=addDirectoryIndex(t,n),a=AbsoluteUrlToCacheName[t]);var c="/";if(!a&&c&&e.request.headers.has("accept")&&e.request.headers.get("accept").includes("text/html")&&isPathWhitelisted(["^((?!(static|api)).)*$"],e.request.url)){var r=new URL(c,self.location);a=AbsoluteUrlToCacheName[r.toString()]}a&&e.respondWith(caches.open(a).then(function(e){return e.keys().then(function(t){return e.match(t[0]).then(function(e){if(e)return e;throw Error("The cache "+a+" is empty.")})})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}});
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This generated service worker JavaScript will precache your site's resources.
// The code needs to be saved in a .js file at the top-level of your site, and registered
// from your pages in order to be used. See
// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js
// for an example of how you can register this script and handle various service worker events.
/* eslint-env worker, serviceworker */
/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren */
'use strict';
/* eslint-disable quotes, comma-spacing */
var PrecacheConfig = [["/","d2c67846acf9a583c29798c30503cbf1"],["/devEvent","c4cdd84093404ee3fe0896070ebde97f"],["/devInfo","c4cdd84093404ee3fe0896070ebde97f"],["/devService","c4cdd84093404ee3fe0896070ebde97f"],["/devState","c4cdd84093404ee3fe0896070ebde97f"],["/devTemplate","c4cdd84093404ee3fe0896070ebde97f"],["/history","d2c67846acf9a583c29798c30503cbf1"],["/logbook","d2c67846acf9a583c29798c30503cbf1"],["/map","df0c87260b6dd990477cda43a2440b1c"],["/states","d2c67846acf9a583c29798c30503cbf1"],["/static/core-7d80cc0e4dea6bc20fa2889be0b3cd15.js","1f35577e9f32a86a03944e5e8d15eab2"],["/static/dev-tools-b7079ac3121b95b9856e5603a6d8a263.html","4ba7c57b48c9d28a1e0d9d7624b83700"],["/static/frontend-805f8dda70419b26daabc8e8f625127f.html","d8eeb403baf5893de8404beec0135d96"],["/static/mdi-758957b7ea989d6beca60e218ea7f7dd.html","4c32b01a3a5b194630963ff7ec4df36f"],["/static/partial-map-c922306de24140afd14f857f927bf8f0.html","853772ea26ac2f4db0f123e20c1ca160"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
/* eslint-enable quotes, comma-spacing */
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
var IgnoreUrlParametersMatching = [/^utm_/];
var addDirectoryIndex = function (originalUrl, index) {
var url = new URL(originalUrl);
if (url.pathname.slice(-1) === '/') {
url.pathname += index;
}
return url.toString();
};
var getCacheBustedUrl = function (url, param) {
param = param || Date.now();
var urlWithCacheBusting = new URL(url);
urlWithCacheBusting.search += (urlWithCacheBusting.search ? '&' : '') +
'sw-precache=' + param;
return urlWithCacheBusting.toString();
};
var isPathWhitelisted = function (whitelist, absoluteUrlString) {
// If the whitelist is empty, then consider all URLs to be whitelisted.
if (whitelist.length === 0) {
return true;
}
// Otherwise compare each path regex to the path of the URL passed in.
var path = (new URL(absoluteUrlString)).pathname;
return whitelist.some(function(whitelistedPathRegex) {
return path.match(whitelistedPathRegex);
});
};
var populateCurrentCacheNames = function (precacheConfig,
cacheNamePrefix, baseUrl) {
var absoluteUrlToCacheName = {};
var currentCacheNamesToAbsoluteUrl = {};
precacheConfig.forEach(function(cacheOption) {
var absoluteUrl = new URL(cacheOption[0], baseUrl).toString();
var cacheName = cacheNamePrefix + absoluteUrl + '-' + cacheOption[1];
currentCacheNamesToAbsoluteUrl[cacheName] = absoluteUrl;
absoluteUrlToCacheName[absoluteUrl] = cacheName;
});
return {
absoluteUrlToCacheName: absoluteUrlToCacheName,
currentCacheNamesToAbsoluteUrl: currentCacheNamesToAbsoluteUrl
};
};
var stripIgnoredUrlParameters = function (originalUrl,
ignoreUrlParametersMatching) {
var url = new URL(originalUrl);
url.search = url.search.slice(1) // Exclude initial '?'
.split('&') // Split into an array of 'key=value' strings
.map(function(kv) {
return kv.split('='); // Split each 'key=value' string into a [key, value] array
})
.filter(function(kv) {
return ignoreUrlParametersMatching.every(function(ignoredRegex) {
return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
});
})
.map(function(kv) {
return kv.join('='); // Join each [key, value] array into a 'key=value' string
})
.join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
return url.toString();
};
var mappings = populateCurrentCacheNames(PrecacheConfig, CacheNamePrefix, self.location);
var AbsoluteUrlToCacheName = mappings.absoluteUrlToCacheName;
var CurrentCacheNamesToAbsoluteUrl = mappings.currentCacheNamesToAbsoluteUrl;
function deleteAllCaches() {
return caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
return caches.delete(cacheName);
})
);
});
}
self.addEventListener('install', function(event) {
event.waitUntil(
// Take a look at each of the cache names we expect for this version.
Promise.all(Object.keys(CurrentCacheNamesToAbsoluteUrl).map(function(cacheName) {
return caches.open(cacheName).then(function(cache) {
// Get a list of all the entries in the specific named cache.
// For caches that are already populated for a given version of a
// resource, there should be 1 entry.
return cache.keys().then(function(keys) {
// If there are 0 entries, either because this is a brand new version
// of a resource or because the install step was interrupted the
// last time it ran, then we need to populate the cache.
if (keys.length === 0) {
// Use the last bit of the cache name, which contains the hash,
// as the cache-busting parameter.
// See https://github.com/GoogleChrome/sw-precache/issues/100
var cacheBustParam = cacheName.split('-').pop();
var urlWithCacheBusting = getCacheBustedUrl(
CurrentCacheNamesToAbsoluteUrl[cacheName], cacheBustParam);
var request = new Request(urlWithCacheBusting,
{credentials: 'same-origin'});
return fetch(request).then(function(response) {
if (response.ok) {
return cache.put(CurrentCacheNamesToAbsoluteUrl[cacheName],
response);
}
console.error('Request for %s returned a response status %d, ' +
'so not attempting to cache it.',
urlWithCacheBusting, response.status);
// Get rid of the empty cache if we can't add a successful response to it.
return caches.delete(cacheName);
});
}
});
});
})).then(function() {
return caches.keys().then(function(allCacheNames) {
return Promise.all(allCacheNames.filter(function(cacheName) {
return cacheName.indexOf(CacheNamePrefix) === 0 &&
!(cacheName in CurrentCacheNamesToAbsoluteUrl);
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
});
}).then(function() {
if (typeof self.skipWaiting === 'function') {
// Force the SW to transition from installing -> active state
self.skipWaiting();
}
})
);
});
if (self.clients && (typeof self.clients.claim === 'function')) {
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
}
self.addEventListener('message', function(event) {
if (event.data.command === 'delete_all') {
console.log('About to delete all caches...');
deleteAllCaches().then(function() {
console.log('Caches deleted.');
event.ports[0].postMessage({
error: null
});
}).catch(function(error) {
console.log('Caches not deleted:', error);
event.ports[0].postMessage({
error: error
});
});
}
});
self.addEventListener('fetch', function(event) {
if (event.request.method === 'GET') {
var urlWithoutIgnoredParameters = stripIgnoredUrlParameters(event.request.url,
IgnoreUrlParametersMatching);
var cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
var directoryIndex = 'index.html';
if (!cacheName && directoryIndex) {
urlWithoutIgnoredParameters = addDirectoryIndex(urlWithoutIgnoredParameters, directoryIndex);
cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
}
var navigateFallback = '';
// Ideally, this would check for event.request.mode === 'navigate', but that is not widely
// supported yet:
// https://code.google.com/p/chromium/issues/detail?id=540967
// https://bugzilla.mozilla.org/show_bug.cgi?id=1209081
if (!cacheName && navigateFallback && event.request.headers.has('accept') &&
event.request.headers.get('accept').includes('text/html') &&
/* eslint-disable quotes, comma-spacing */
isPathWhitelisted([], event.request.url)) {
/* eslint-enable quotes, comma-spacing */
var navigateFallbackUrl = new URL(navigateFallback, self.location);
cacheName = AbsoluteUrlToCacheName[navigateFallbackUrl.toString()];
}
if (cacheName) {
event.respondWith(
// Rely on the fact that each cache we manage should only have one entry, and return that.
caches.open(cacheName).then(function(cache) {
return cache.keys().then(function(keys) {
return cache.match(keys[0]).then(function(response) {
if (response) {
return response;
}
// If for some reason the response was deleted from the cache,
// raise and exception and fall back to the fetch() triggered in the catch().
throw Error('The cache ' + cacheName + ' is empty.');
});
});
}).catch(function(e) {
console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
return fetch(event.request);
})
);
}
}
});

View File

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

View File

@@ -14,6 +14,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun from homeassistant.components import recorder, sun
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START, from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
@@ -24,7 +25,7 @@ from homeassistant.helpers import template
from homeassistant.helpers.entity import split_entity_id from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook" 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})|)') 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) hass.wsgi.register_view(LogbookView)
register_built_in_panel(hass, 'logbook', 'Logbook',
'mdi:format-list-bulleted-type')
hass.services.register(DOMAIN, 'log', log_message, hass.services.register(DOMAIN, 'log', log_message,
schema=LOG_MESSAGE_SCHEMA) schema=LOG_MESSAGE_SCHEMA)
return True return True

View File

@@ -0,0 +1,31 @@
"""Add an iframe panel to Home Assistant."""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'panel_iframe'
DEPENDENCIES = ['frontend']
CONF_TITLE = 'title'
CONF_ICON = 'icon'
CONF_URL = 'url'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: {
vol.Optional(CONF_TITLE): cv.string,
vol.Optional(CONF_ICON): cv.icon,
# pylint: disable=no-value-for-parameter
vol.Required(CONF_URL): vol.Url(),
}})}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup iframe frontend panels."""
for url_name, info in config[DOMAIN].items():
register_built_in_panel(
hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
url_name, {'url': info[CONF_URL]})
return True

View File

@@ -39,7 +39,8 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int), vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)), vol.Range(min=1)),
vol.Optional(CONF_DB_URL): vol.Url(''), # pylint: disable=no-value-for-parameter
vol.Optional(CONF_DB_URL): vol.Url(),
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)

View File

@@ -2,36 +2,21 @@
cd "$(dirname "$0")/.." 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 npm run frontend_prod
cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
cp build/frontend.html .. cp -r build/* ..
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
node script/sw-precache.js node script/sw-precache.js
cp build/service_worker.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 # Generate the MD5 hash of the new frontend
cd ../.. cd ../../../..
echo '"""DO NOT MODIFY. Auto-generated by build_frontend script."""' > version.py script/fingerprint_frontend.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

38
script/fingerprint_frontend.py Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Generate a file with all md5 hashes of the assets."""
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 #!/usr/bin/env python3
"""Download the latest Polymer v1 iconset for materialdesignicons.com.""" """Download the latest Polymer v1 iconset for materialdesignicons.com."""
import hashlib
import gzip import gzip
import os import os
import re import re
import requests import requests
import sys import sys
from fingerprint_frontend import fingerprint
GETTING_STARTED_URL = ('https://raw.githubusercontent.com/Templarian/' GETTING_STARTED_URL = ('https://raw.githubusercontent.com/Templarian/'
'MaterialDesign/master/site/getting-started.savvy') 'MaterialDesign/master/site/getting-started.savvy')
DOWNLOAD_LINK = re.compile(r'(/api/download/polymer/v1/([A-Z0-9-]{36}))') DOWNLOAD_LINK = re.compile(r'(/api/download/polymer/v1/([A-Z0-9-]{36}))')
START_ICONSET = '<iron-iconset-svg' START_ICONSET = '<iron-iconset-svg'
CUR_VERSION = re.compile(r'VERSION = "([A-Za-z0-9]{32})"')
OUTPUT_BASE = os.path.join('homeassistant', 'components', 'frontend') 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 = os.path.join(OUTPUT_BASE, 'www_static', 'mdi.html')
ICONSET_OUTPUT_GZ = os.path.join(OUTPUT_BASE, 'www_static', 'mdi.html.gz') 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(): def get_remote_version():
"""Get current version and download link.""" """Get current version and download link."""
gs_page = requests.get(GETTING_STARTED_URL).text gs_page = requests.get(GETTING_STARTED_URL).text
@@ -43,10 +29,7 @@ def get_remote_version():
print("Unable to find download link") print("Unable to find download link")
sys.exit() sys.exit()
url = 'https://materialdesignicons.com' + mdi_download.group(1) return 'https://materialdesignicons.com' + mdi_download.group(1)
version = mdi_download.group(2).replace('-', '')
return version, url
def clean_component(source): def clean_component(source):
@@ -54,7 +37,7 @@ def clean_component(source):
return source[source.index(START_ICONSET):] return source[source.index(START_ICONSET):]
def write_component(version, source): def write_component(source):
"""Write component.""" """Write component."""
with open(ICONSET_OUTPUT, 'w') as outp: with open(ICONSET_OUTPUT, 'w') as outp:
print('Writing icons to', ICONSET_OUTPUT) print('Writing icons to', ICONSET_OUTPUT)
@@ -64,12 +47,6 @@ def write_component(version, source):
print('Writing icons gz to', ICONSET_OUTPUT_GZ) print('Writing icons gz to', ICONSET_OUTPUT_GZ)
outp.write(source.encode('utf-8')) 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(): def main():
"""Main section of the script.""" """Main section of the script."""
@@ -79,19 +56,11 @@ def main():
print("materialdesignicons.com icon updater") print("materialdesignicons.com icon updater")
local_version = get_local_version() remote_url = get_remote_version()
# The remote version is not reliable.
_, remote_url = get_remote_version()
source = clean_component(requests.get(remote_url).text) 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') print('Updated to latest version')
if __name__ == '__main__': if __name__ == '__main__':

View File

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

View File

@@ -0,0 +1,76 @@
"""The tests for the panel_iframe component."""
import unittest
from unittest.mock import patch
from homeassistant import bootstrap
from homeassistant.components import frontend
from tests.common import get_test_home_assistant
class TestPanelIframe(unittest.TestCase):
"""Test the panel_iframe component."""
def setup_method(self, method):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
frontend.PANELS = {}
def teardown_method(self, method):
"""Stop everything that was started."""
self.hass.stop()
frontend.PANELS = {}
def test_wrong_config(self):
"""Test setup with wrong configuration."""
to_try = [
{'invalid space': {
'url': 'https://home-assistant.io'}},
{'router': {
'url': 'not-a-url'}}]
for conf in to_try:
assert not bootstrap.setup_component(
self.hass, 'panel_iframe', {
'panel_iframe': conf
})
@patch.dict('homeassistant.components.frontend.FINGERPRINTS', {
'panels/ha-panel-iframe.html': 'md5md5'})
def test_correct_config(self):
"""Test correct config."""
assert bootstrap.setup_component(
self.hass, 'panel_iframe', {
'panel_iframe': {
'router': {
'icon': 'mdi:network-wireless',
'title': 'Router',
'url': 'http://192.168.1.1',
},
'weather': {
'icon': 'mdi:weather',
'title': 'Weather',
'url': 'https://www.wunderground.com/us/ca/san-diego',
},
},
})
# 5 dev tools + map are automatically loaded
assert len(frontend.PANELS) == 8
assert frontend.PANELS['router'] == {
'component_name': 'iframe',
'config': {'url': 'http://192.168.1.1'},
'icon': 'mdi:network-wireless',
'title': 'Router',
'url': '/frontend/panels/iframe-md5md5.html',
'url_name': 'router'
}
assert frontend.PANELS['weather'] == {
'component_name': 'iframe',
'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'},
'icon': 'mdi:weather',
'title': 'Weather',
'url': '/frontend/panels/iframe-md5md5.html',
'url_name': 'weather',
}