diff --git a/.gitignore b/.gitignore index 2edbd42b839..64ab38f2da8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ config/custom_components/* !config/custom_components/example.py !config/custom_components/hello_world.py !config/custom_components/mqtt_example.py +!config/custom_components/react_panel tests/testing_config/deps tests/testing_config/home-assistant.log diff --git a/config/custom_components/react_panel/__init__.py b/config/custom_components/react_panel/__init__.py new file mode 100644 index 00000000000..57073b8cddc --- /dev/null +++ b/config/custom_components/react_panel/__init__.py @@ -0,0 +1,30 @@ +""" +Custom panel example showing TodoMVC using React. + +Will add a panel to control lights and switches using React. Allows configuring +the title via configuration.yaml: + +react_panel: + title: 'home' + +""" +import os + +from homeassistant.components.frontend import register_panel + +DOMAIN = 'react_panel' +DEPENDENCIES = ['frontend'] + +PANEL_PATH = os.path.join(os.path.dirname(__file__), 'panel.html') + + +def setup(hass, config): + """Initialize custom panel.""" + title = config.get(DOMAIN, {}).get('title') + + config = None if title is None else {'title': title} + + register_panel(hass, 'react', PANEL_PATH, + title='TodoMVC', icon='mdi:checkbox-marked-outline', + config=config) + return True diff --git a/config/custom_components/react_panel/panel.html b/config/custom_components/react_panel/panel.html new file mode 100644 index 00000000000..12b473cc466 --- /dev/null +++ b/config/custom_components/react_panel/panel.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 021f209a56f..3925170694e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,4 +1,5 @@ """Handle the frontend for Home Assistant.""" +import hashlib import logging import os @@ -25,20 +26,27 @@ def register_built_in_panel(hass, component_name, title=None, icon=None, # pylint: disable=too-many-arguments path = 'panels/ha-panel-{}.html'.format(component_name) + if hass.wsgi.development: + url = ('/static/home-assistant-polymer/panels/' + '{0}/ha-panel-{0}.html'.format(component_name)) + else: + url = None # use default url generate mechanism + register_panel(hass, component_name, os.path.join(STATIC_PATH, path), - FINGERPRINTS[path], title, icon, url_name, config) + FINGERPRINTS[path], title, icon, url_name, url, config) -def register_panel(hass, component_name, path, md5, title=None, icon=None, - url_name=None, config=None): +def register_panel(hass, component_name, path, md5=None, title=None, icon=None, + url_name=None, url=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) + md5: the md5 hash of the web component (for versioning, optional) 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) + url: for the web component (for dev environment, optional) config: config to be passed into the web component Warning: this API will probably change. Use at own risk. @@ -50,8 +58,13 @@ def register_panel(hass, component_name, path, md5, title=None, icon=None, 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) + _LOGGER.error('Panel %s component does not exist: %s', + component_name, path) + return + + if md5 is None: + with open(path) as fil: + md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest() data = { 'url_name': url_name, @@ -65,9 +78,8 @@ def register_panel(hass, component_name, path, md5, title=None, icon=None, 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)) + if url is not None: + data['url'] = url else: url = URL_PANEL_COMPONENT.format(component_name) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index a1225558535..74e84e8c06d 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -2,7 +2,7 @@ FINGERPRINTS = { "core.js": "bc78f21f5280217aa2c78dfc5848134f", - "frontend.html": "6fdf8282937005d3e4395f456199b118", + "frontend.html": "6c52e8cb797bafa3124d936af5ce1fcc", "mdi.html": "f6c6cc64c2ec38a80e91f801b41119b3", "panels/ha-panel-dev-event.html": "20327fbd4fb0370aec9be4db26fd723f", "panels/ha-panel-dev-info.html": "28e0a19ceb95aa714fd53228d9983a49", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 77bd761fe78..9dafede2a71 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -3,4 +3,4 @@ e._bubble()}function Bn(e,t){return $n(this,e,t,1)}function Jn(e,t){return $n(th },_distributeDirtyRoots:function(){for(var e,t=this.shadyRoot._dirtyRoots,o=0,i=t.length;o0?~setTimeout(e,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(e<0)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;t \ No newline at end of file +}var r=window.requestAnimationFrame;window.requestAnimationFrame=function(t){return r(function(e){window.document.timeline._updateAnimationsPromises(),t(e),window.document.timeline._updateAnimationsPromises()})},e.AnimationTimeline=function(){this._animations=[],this.currentTime=void 0},e.AnimationTimeline.prototype={getAnimations:function(){return this._discardAnimations(),this._animations.slice()},_updateAnimationsPromises:function(){e.animationsWithPromises=e.animationsWithPromises.filter(function(t){return t._updatePromises()})},_discardAnimations:function(){this._updateAnimationsPromises(),this._animations=this._animations.filter(function(t){return"finished"!=t.playState&&"idle"!=t.playState})},_play:function(t){var i=new e.Animation(t,this);return this._animations.push(i),e.restartWebAnimationsNextTick(),i._updatePromises(),i._animation.play(),i._updatePromises(),i},play:function(t){return t&&t.remove(),this._play(t)}};var o=!1;e.restartWebAnimationsNextTick=function(){o||(o=!0,requestAnimationFrame(n))};var a=new e.AnimationTimeline;e.timeline=a;try{Object.defineProperty(window.document,"timeline",{configurable:!0,get:function(){return a}})}catch(t){}try{window.document.timeline=a}catch(t){}}(c,e,f),function(t,e,i){e.animationsWithPromises=[],e.Animation=function(e,i){if(this.id="",e&&e._id&&(this.id=e._id),this.effect=e,e&&(e._animation=this),!i)throw new Error("Animation with null timeline is not supported");this._timeline=i,this._sequenceNumber=t.sequenceNumber++,this._holdTime=0,this._paused=!1,this._isGroup=!1,this._animation=null,this._childAnimations=[],this._callback=null,this._oldPlayState="idle",this._rebuildUnderlyingAnimation(),this._animation.cancel(),this._updatePromises()},e.Animation.prototype={_updatePromises:function(){var t=this._oldPlayState,e=this.playState;return this._readyPromise&&e!==t&&("idle"==e?(this._rejectReadyPromise(),this._readyPromise=void 0):"pending"==t?this._resolveReadyPromise():"pending"==e&&(this._readyPromise=void 0)),this._finishedPromise&&e!==t&&("idle"==e?(this._rejectFinishedPromise(),this._finishedPromise=void 0):"finished"==e?this._resolveFinishedPromise():"finished"==t&&(this._finishedPromise=void 0)),this._oldPlayState=this.playState,this._readyPromise||this._finishedPromise},_rebuildUnderlyingAnimation:function(){this._updatePromises();var t,i,n,r,o=!!this._animation;o&&(t=this.playbackRate,i=this._paused,n=this.startTime,r=this.currentTime,this._animation.cancel(),this._animation._wrapper=null,this._animation=null),(!this.effect||this.effect instanceof window.KeyframeEffect)&&(this._animation=e.newUnderlyingAnimationForKeyframeEffect(this.effect),e.bindAnimationForKeyframeEffect(this)),(this.effect instanceof window.SequenceEffect||this.effect instanceof window.GroupEffect)&&(this._animation=e.newUnderlyingAnimationForGroup(this.effect),e.bindAnimationForGroup(this)),this.effect&&this.effect._onsample&&e.bindAnimationForCustomEffect(this),o&&(1!=t&&(this.playbackRate=t),null!==n?this.startTime=n:null!==r?this.currentTime=r:null!==this._holdTime&&(this.currentTime=this._holdTime),i&&this.pause()),this._updatePromises()},_updateChildren:function(){if(this.effect&&"idle"!=this.playState){var t=this.effect._timing.delay;this._childAnimations.forEach(function(i){this._arrangeChildren(i,t),this.effect instanceof window.SequenceEffect&&(t+=e.groupChildDuration(i.effect))}.bind(this))}},_setExternalAnimation:function(t){if(this.effect&&this._isGroup)for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 54153e3662f..bbc4f6c09f9 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 974c75b9ed7..519533a4af4 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 974c75b9ed7a30b00c64aa8d163f120d3995d9e7 +Subproject commit 519533a4af4d5f16ec41dbc4d298d7566116fdad diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 9b7a9f7a188..91866479afe 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function deleteAllCaches(){return caches.keys().then(function(e){return Promise.all(e.map(function(e){return caches.delete(e)}))})}var PrecacheConfig=[["/","1dc05a9f1b8d47fce5cc5f8db49de8d8"],["/frontend/panels/dev-event-20327fbd4fb0370aec9be4db26fd723f.html","a9b6eced242c1934a331c05c30e22148"],["/frontend/panels/dev-info-28e0a19ceb95aa714fd53228d9983a49.html","75862082477c802a12d2bf8705990d85"],["/frontend/panels/dev-service-85fd5b48600418bb5a6187539a623c38.html","353e4d80fedbcde9b51e08a78a9ddb86"],["/frontend/panels/dev-state-25d84d7b7aea779bb3bb3cd6c155f8d9.html","7fc5b1880ba4a9d6e97238e8e5a44d69"],["/frontend/panels/dev-template-d079abf61cff9690f828cafb0d29b7e7.html","6e512a2ba0eb7aeba956ca51048e701e"],["/frontend/panels/map-dfe141a3fa5fd403be554def1dd039a9.html","f061ec88561705f7787a00289450c006"],["/static/core-bc78f21f5280217aa2c78dfc5848134f.js","a09b7ee4108fae1f93c10e14a4bfd675"],["/static/frontend-6fdf8282937005d3e4395f456199b118.html","fa094e1f884700a35ba5195bcd226dda"],["/static/mdi-f6c6cc64c2ec38a80e91f801b41119b3.html","e010f32322ed6f66916c7c09dbba4acd"],["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={},r={};return e.forEach(function(e){var c=new URL(e[0],a).toString(),o=t+c+"-"+e[1];r[o]=c,n[c]=o}),{absoluteUrlToCacheName:n,currentCacheNamesToAbsoluteUrl:r}},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(),r=getCacheBustedUrl(CurrentCacheNamesToAbsoluteUrl[e],n),c=new Request(r,{credentials:"same-origin"});return fetch(c).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.",r,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 r="/";if(!a&&r&&e.request.headers.has("accept")&&e.request.headers.get("accept").includes("text/html")&&isPathWhitelisted(["^((?!(static|api|service_worker.js)).)*$"],e.request.url)){var c=new URL(r,self.location);a=AbsoluteUrlToCacheName[c.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)}))}}); \ No newline at end of file +"use strict";function deleteAllCaches(){return caches.keys().then(function(e){return Promise.all(e.map(function(e){return caches.delete(e)}))})}var PrecacheConfig=[["/","2d9bbabfa2dc5f2a651ff1141d7e306c"],["/frontend/panels/dev-event-20327fbd4fb0370aec9be4db26fd723f.html","a9b6eced242c1934a331c05c30e22148"],["/frontend/panels/dev-info-28e0a19ceb95aa714fd53228d9983a49.html","75862082477c802a12d2bf8705990d85"],["/frontend/panels/dev-service-85fd5b48600418bb5a6187539a623c38.html","353e4d80fedbcde9b51e08a78a9ddb86"],["/frontend/panels/dev-state-25d84d7b7aea779bb3bb3cd6c155f8d9.html","7fc5b1880ba4a9d6e97238e8e5a44d69"],["/frontend/panels/dev-template-d079abf61cff9690f828cafb0d29b7e7.html","6e512a2ba0eb7aeba956ca51048e701e"],["/frontend/panels/map-dfe141a3fa5fd403be554def1dd039a9.html","f061ec88561705f7787a00289450c006"],["/static/core-bc78f21f5280217aa2c78dfc5848134f.js","a09b7ee4108fae1f93c10e14a4bfd675"],["/static/frontend-6c52e8cb797bafa3124d936af5ce1fcc.html","a460549fe50b2e7c9cadd94d682c9ed7"],["/static/mdi-f6c6cc64c2ec38a80e91f801b41119b3.html","e010f32322ed6f66916c7c09dbba4acd"],["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|service_worker.js)).)*$"],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)}))}}); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/service_worker.js.gz b/homeassistant/components/frontend/www_static/service_worker.js.gz index 25fd33f6c22..787dd89c271 100644 Binary files a/homeassistant/components/frontend/www_static/service_worker.js.gz and b/homeassistant/components/frontend/www_static/service_worker.js.gz differ