diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5f59f760d0b..7c11f15862f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -16,7 +16,7 @@ from homeassistant.core import CoreState from homeassistant import config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START) + SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition @@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv +from homeassistant.components.frontend import register_built_in_panel DOMAIN = 'automation' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -81,6 +82,7 @@ _TRIGGER_SCHEMA = vol.All( _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.Schema({ + CONF_ID: cv.string, CONF_ALIAS: cv.string, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, @@ -139,6 +141,14 @@ def reload(hass): hass.services.call(DOMAIN, SERVICE_RELOAD) +def async_reload(hass): + """Reload the automation from config. + + Returns a coroutine object. + """ + return hass.services.async_call(DOMAIN, SERVICE_RELOAD) + + @asyncio.coroutine def async_setup(hass, config): """Set up the automation.""" @@ -215,15 +225,20 @@ def async_setup(hass, config): DOMAIN, service, turn_onoff_service_handler, descriptions.get(service), schema=SERVICE_SCHEMA) + if 'frontend' in hass.config.components: + register_built_in_panel(hass, 'automation', 'Automations', + 'mdi:playlist-play') + return True class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" - def __init__(self, name, async_attach_triggers, cond_func, async_action, - hidden, initial_state): + def __init__(self, automation_id, name, async_attach_triggers, cond_func, + async_action, hidden, initial_state): """Initialize an automation entity.""" + self._id = automation_id self._name = name self._async_attach_triggers = async_attach_triggers self._async_detach_triggers = None @@ -346,6 +361,16 @@ class AutomationEntity(ToggleEntity): self.async_trigger) yield from self.async_update_ha_state() + @property + def device_state_attributes(self): + """Return automation attributes.""" + if self._id is None: + return None + + return { + CONF_ID: self._id + } + @asyncio.coroutine def _async_process_config(hass, config, component): @@ -359,6 +384,7 @@ def _async_process_config(hass, config, component): conf = config[config_key] for list_no, config_block in enumerate(conf): + automation_id = config_block.get(CONF_ID) name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no) @@ -383,8 +409,8 @@ def _async_process_config(hass, config, component): config_block.get(CONF_TRIGGER, []), name ) entity = AutomationEntity( - name, async_attach_triggers, cond_func, action, hidden, - initial_state) + automation_id, name, async_attach_triggers, cond_func, action, + hidden, initial_state) entities.append(entity) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 1255043b6b5..0bc44501e28 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,7 +5,7 @@ import os import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID from homeassistant.setup import ( async_prepare_setup_platform, ATTR_COMPONENT) from homeassistant.components.frontend import register_built_in_panel @@ -14,8 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] -SECTIONS = ('core', 'group', 'hassbian') -ON_DEMAND = ('zwave', ) +SECTIONS = ('core', 'group', 'hassbian', 'automation') +ON_DEMAND = ('zwave') @asyncio.coroutine @@ -60,7 +60,7 @@ def async_setup(hass, config): return True -class EditKeyBasedConfigView(HomeAssistantView): +class BaseEditConfigView(HomeAssistantView): """Configure a Group endpoint.""" def __init__(self, component, config_type, path, key_schema, data_schema, @@ -73,13 +73,29 @@ class EditKeyBasedConfigView(HomeAssistantView): self.data_schema = data_schema self.post_write_hook = post_write_hook + def _empty_config(self): + """Empty config if file not found.""" + raise NotImplementedError + + def _get_value(self, data, config_key): + """Get value.""" + raise NotImplementedError + + def _write_value(self, data, config_key, new_value): + """Set value.""" + raise NotImplementedError + @asyncio.coroutine def get(self, request, config_key): """Fetch device specific config.""" hass = request.app['hass'] - current = yield from hass.loop.run_in_executor( - None, _read, hass.config.path(self.path)) - return self.json(current.get(config_key, {})) + current = yield from self.read_config(hass) + value = self._get_value(current, config_key) + + if value is None: + return self.json_message('Resource not found', 404) + + return self.json(value) @asyncio.coroutine def post(self, request, config_key): @@ -104,10 +120,10 @@ class EditKeyBasedConfigView(HomeAssistantView): hass = request.app['hass'] path = hass.config.path(self.path) - current = yield from hass.loop.run_in_executor(None, _read, path) - current.setdefault(config_key, {}).update(data) + current = yield from self.read_config(hass) + self._write_value(current, config_key, data) - yield from hass.loop.run_in_executor(None, _write, path, current) + yield from hass.async_add_job(_write, path, current) if self.post_write_hook is not None: hass.async_add_job(self.post_write_hook(hass)) @@ -116,13 +132,59 @@ class EditKeyBasedConfigView(HomeAssistantView): 'result': 'ok', }) + @asyncio.coroutine + def read_config(self, hass): + """Read the config.""" + current = yield from hass.async_add_job( + _read, hass.config.path(self.path)) + if not current: + current = self._empty_config() + return current + + +class EditKeyBasedConfigView(BaseEditConfigView): + """Configure a list of entries.""" + + def _empty_config(self): + """Return an empty config.""" + return {} + + def _get_value(self, data, config_key): + """Get value.""" + return data.get(config_key, {}) + + def _write_value(self, data, config_key, new_value): + """Set value.""" + data.setdefault(config_key, {}).update(new_value) + + +class EditIdBasedConfigView(BaseEditConfigView): + """Configure key based config entries.""" + + def _empty_config(self): + """Return an empty config.""" + return [] + + def _get_value(self, data, config_key): + """Get value.""" + return next( + (val for val in data if val.get(CONF_ID) == config_key), None) + + def _write_value(self, data, config_key, new_value): + """Set value.""" + value = self._get_value(data, config_key) + + if value is None: + value = {CONF_ID: config_key} + data.append(value) + + value.update(new_value) + def _read(path): """Read YAML helper.""" if not os.path.isfile(path): - with open(path, 'w'): - pass - return {} + return None return load_yaml(path) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py new file mode 100644 index 00000000000..64eccfaa2b8 --- /dev/null +++ b/homeassistant/components/config/automation.py @@ -0,0 +1,20 @@ +"""Provide configuration end points for Z-Wave.""" +import asyncio + +from homeassistant.components.config import EditIdBasedConfigView +from homeassistant.components.automation import ( + PLATFORM_SCHEMA, DOMAIN, async_reload) +import homeassistant.helpers.config_validation as cv + + +CONFIG_PATH = 'automations.yaml' + + +@asyncio.coroutine +def async_setup(hass): + """Set up the Automation config API.""" + hass.http.register_view(EditIdBasedConfigView( + DOMAIN, 'config', CONFIG_PATH, cv.string, + PLATFORM_SCHEMA, post_write_hook=async_reload + )) + return True diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 0d649344862..f92bb64ff69 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,18 +1,19 @@ """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" FINGERPRINTS = { - "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", - "core.js": "5d08475f03adb5969bd31855d5ca0cfd", + "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", + "core.js": "8cc30e2ad9ee3df44fe7a17507099d88", "frontend.html": "5999c8fac69c503b846672cae75a12b0", "mdi.html": "f407a5a57addbe93817ee1b244d33fbe", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", + "panels/ha-panel-automation.html": "cc6fe23a97c1974b9f4165a7692bb280", "panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1", "panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2", "panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d", "panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed", "panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750", "panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b", - "panels/ha-panel-hassio.html": "23d175b6744c20e2fdf475b6efdaa1d3", + "panels/ha-panel-hassio.html": "41fc94a5dc9247ed7efa112614491c71", "panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", diff --git a/homeassistant/components/frontend/www_static/compatibility.js b/homeassistant/components/frontend/www_static/compatibility.js index c152c50ddfa..927b37e68ce 100644 --- a/homeassistant/components/frontend/www_static/compatibility.js +++ b/homeassistant/components/frontend/www_static/compatibility.js @@ -1 +1 @@ -!(function(){"use strict";function e(e,r){var t=arguments;if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var n=Object(e),o=1;o \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz new file mode 100644 index 00000000000..e717b38ea4b Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html index 80d1686acf0..ca491348298 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html @@ -14,4 +14,4 @@ computeInstallStatus(addon) { return (addon && addon.installed) || 'Not installed'; }, -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz index 5ed1205a999..2a18410d990 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 445c8d0b9df..7315b62aa3d 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","c255cdae4bfd691ee9fa43e38e8dc462"],["/frontend/panels/dev-event-2db9c218065ef0f61d8d08db8093cad2.html","b5b751e49b1bba55f633ae0d7a92677d"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-415552027cb083badeff5f16080410ed.html","a4b1ec9bfa5bc3529af7783ae56cb55c"],["/frontend/panels/dev-state-d70314913b8923d750932367b1099750.html","c61b5b1461959aac106400e122993e9e"],["/frontend/panels/dev-template-567fbf86735e1b891e40c2f4060fec9b.html","d2853ecf45de1dbadf49fe99a7424ef3"],["/frontend/panels/map-31c592c239636f91e07c7ac232a5ebc4.html","182580419ce2c935ae6ec65502b6db96"],["/static/compatibility-83d9c77748dafa9db49ae77d7f3d8fb0.js","5f05c83be2b028d577962f9625904806"],["/static/core-5d08475f03adb5969bd31855d5ca0cfd.js","1cd99ba798bfcff9768c9d2bb2f58a7c"],["/static/frontend-5999c8fac69c503b846672cae75a12b0.html","d6ce8eb348fbea599933b2a72beb1337"],["/static/mdi-f407a5a57addbe93817ee1b244d33fbe.html","5459090f217c77747b08d06e0bf73388"],["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","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.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("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url)&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n