Merge pull request #2537 from home-assistant/frontend-panels
Frontend panels
@@ -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(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -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')
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
|
||||||
VERSION = "758957b7ea989d6beca60e218ea7f7dd"
|
|
@@ -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>
|
||||||
|
@@ -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"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
@@ -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"
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
31
homeassistant/components/panel_iframe.py
Normal 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
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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
@@ -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()
|
@@ -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__':
|
||||||
|
@@ -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):
|
||||||
|
76
tests/components/test_panel_iframe.py
Normal 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',
|
||||||
|
}
|