mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Add automation editor (#275)
* Add automation editor * Build JS before running tests * Add browser warning * Re-order from/to in state
This commit is contained in:
parent
9e7dc4a921
commit
ca82a411aa
20
.babelrc
Normal file
20
.babelrc
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"es2015",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers",
|
||||
"transform-object-rest-spread",
|
||||
[
|
||||
"transform-react-jsx",
|
||||
{
|
||||
"pragma":"h"
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
30
.eslintrc
30
.eslintrc
@ -1,5 +1,16 @@
|
||||
{
|
||||
"extends": "airbnb-base",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "h"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": false,
|
||||
"__DEMO__": false,
|
||||
@ -26,9 +37,24 @@
|
||||
"no-continue": 0,
|
||||
"no-param-reassign": 0,
|
||||
"no-multi-assign": 0,
|
||||
"radix": 0
|
||||
"radix": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"react/jsx-no-bind": [2, { "ignoreRefs": true }],
|
||||
"react/jsx-no-duplicate-props": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/no-string-refs": 2,
|
||||
"react/require-render-return": 2,
|
||||
"react/no-find-dom-node": 2,
|
||||
"react/no-is-mounted": 2,
|
||||
"react/jsx-no-comment-textnodes": 2,
|
||||
"react/jsx-curly-spacing": 2,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2
|
||||
},
|
||||
"plugins": [
|
||||
"html"
|
||||
"html",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ addons:
|
||||
packages:
|
||||
- google-chrome-stable
|
||||
script:
|
||||
- npm run js_prod
|
||||
- npm run test
|
||||
- xvfb-run wct
|
||||
- if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --plugin sauce; fi
|
||||
|
23
package.json
23
package.json
@ -10,42 +10,49 @@
|
||||
"clean": "rm -rf build/* build-temp/*",
|
||||
"js_dev": "node script/gen-service-worker.js && npm run watch_ru_all",
|
||||
"js_dev_demo": "BUILD_DEMO=1 npm run watch_ru_all",
|
||||
"js_prod": "BUILD_DEV=0 npm run ru_all && script/optimize-js.js",
|
||||
"js_prod": "BUILD_DEV=0 npm run ru_all",
|
||||
"js_demo": "BUILD_DEV=0 BUILD_DEMO=1 npm run ru_all",
|
||||
"frontend_html": "node script/vulcanize.js",
|
||||
"frontend_prod": "npm run js_prod && npm run frontend_html",
|
||||
"frontend_demo": "npm run js_demo && npm run frontend_html",
|
||||
"ru_all": "npm run ru_core && npm run ru_compatibility && npm run ru_demo",
|
||||
"ru_all": "npm run ru_automation && npm run ru_core && npm run ru_compatibility && npm run ru_demo",
|
||||
"ru_automation": "rollup --config rollup/automation.js",
|
||||
"ru_core": "rollup --config rollup/core.js",
|
||||
"ru_compatibility": "rollup --config rollup/compatibility.js",
|
||||
"ru_demo": "rollup --config rollup/demo.js",
|
||||
"watch_ru_all": "(npm run watch_ru_core & npm run watch_ru_compatibility & npm run watch_ru_demo) && wait",
|
||||
"watch_ru_all": "(npm run watch_ru_automation & npm run watch_ru_core & npm run watch_ru_compatibility & npm run watch_ru_demo) && wait",
|
||||
"watch_ru_automation": "rollup --config rollup/automation.js --watch --sourcemap inline",
|
||||
"watch_ru_core": "rollup --config rollup/core.js --watch --sourcemap inline",
|
||||
"watch_ru_compatibility": "rollup --config rollup/compatibility.js --watch --sourcemap inline",
|
||||
"watch_ru_demo": "rollup --config rollup/demo.js --watch --sourcemap inline",
|
||||
"lint_js": "eslint src panels --ext html",
|
||||
"lint_js": "eslint src panels preact-src --ext js,html",
|
||||
"lint_html": "ls -1 src/home-assistant.html panels/**/ha-panel-*.html | xargs polymer lint --input",
|
||||
"test": "npm run lint_js && npm run lint_html"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"home-assistant-js-websocket": "^1.1.0"
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"home-assistant-js-websocket": "^1.1.0",
|
||||
"preact": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.23.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"bower": "^1.8.0",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-airbnb-base": "^11.1.3",
|
||||
"eslint-plugin-html": "^2.0.1",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-react": "^7.0.0",
|
||||
"html-minifier": "^3.4.3",
|
||||
"hydrolysis": "^1.24.1",
|
||||
"optimize-js": "^1.0.3",
|
||||
"polymer-cli": "^0.17.0",
|
||||
"polymer-lint": "^0.8.3",
|
||||
"rollup": "^0.41.6",
|
||||
"rollup-plugin-buble": "^0.15.0",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-replace": "^1.1.1",
|
||||
|
10
panels/automation/editor.js
Normal file
10
panels/automation/editor.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { h, render } from 'preact';
|
||||
import Automation from '../../preact-src/automation';
|
||||
|
||||
window.AutomationEditor = function (mountEl, props, mergeEl) {
|
||||
return render(h(Automation, props), mountEl, mergeEl);
|
||||
};
|
||||
|
||||
window.unmountPreact = function (mountEl, mergeEl) {
|
||||
render(() => null, mountEl, mergeEl);
|
||||
};
|
214
panels/automation/ha-automation-editor.html
Normal file
214
panels/automation/ha-automation-editor.html
Normal file
@ -0,0 +1,214 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../bower_components/app-layout/app-header-layout/app-header-layout.html">
|
||||
<link rel="import" href="../../bower_components/app-layout/app-header/app-header.html">
|
||||
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
|
||||
<link rel="import" href="../../bower_components/paper-card/paper-card.html">
|
||||
<link rel="import" href="../../bower_components/paper-item/paper-item.html">
|
||||
<link rel="import" href="../../bower_components/paper-item/paper-item-body.html">
|
||||
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../bower_components/paper-dropdown-menu/paper-dropdown-menu-light.html">
|
||||
<link rel="import" href="../../bower_components/paper-listbox/paper-listbox.html">
|
||||
<link rel="import" href="../../bower_components/paper-menu-button/paper-menu-button.html">
|
||||
|
||||
<link rel="import" href="../config/ha-config-section.html">
|
||||
|
||||
<script src='../../build/editor.js'></script>
|
||||
|
||||
<dom-module id="ha-automation-editor">
|
||||
<template>
|
||||
<style include="ha-style">
|
||||
.errors {
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.triggers,
|
||||
.script {
|
||||
margin-top: -16px;
|
||||
}
|
||||
.triggers paper-card,
|
||||
.script paper-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.add-card paper-button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-menu paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
span[slot=introduction] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header fixed>
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon='mdi:arrow-left'
|
||||
on-tap='backTapped'
|
||||
></paper-icon-button>
|
||||
<div main-title>Automation [[name]]</div>
|
||||
<paper-icon-button
|
||||
icon='mdi:content-save'
|
||||
on-tap='saveAutomation'
|
||||
disabled='[[!dirty]]'
|
||||
></paper-icon-button>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class='content'>
|
||||
<template is='dom-if' if='[[errors]]'>
|
||||
<div class='errors'>[[errors]]</div>
|
||||
</template>
|
||||
<div id='root'></div>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'ha-automation-editor',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
dirty: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
config: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
automation: {
|
||||
type: Object,
|
||||
observer: 'automationChanged',
|
||||
},
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
computed: 'computeName(automation)'
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
observer: 'isWideChanged',
|
||||
},
|
||||
},
|
||||
|
||||
attached: function () {
|
||||
this.configChanged = this.configChanged.bind(this);
|
||||
},
|
||||
|
||||
detached: function () {
|
||||
if (this._rendered) {
|
||||
window.unmountPreact(this._rendered);
|
||||
}
|
||||
},
|
||||
|
||||
configChanged: function (config) {
|
||||
this.config = config;
|
||||
this.errors = null;
|
||||
this.dirty = true;
|
||||
this._updateComponent(config);
|
||||
},
|
||||
|
||||
automationChanged: function (newVal, oldVal) {
|
||||
if (!newVal) return;
|
||||
if (!this.hass) {
|
||||
setTimeout(this.automationChanged.bind(this, newVal, oldVal), 0);
|
||||
return;
|
||||
}
|
||||
if (oldVal && oldVal.attributes.id === newVal.attributes.id) {
|
||||
return;
|
||||
}
|
||||
this.hass.callApi('get', 'config/automation/config/' + newVal.attributes.id)
|
||||
.then(function (config) {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
['trigger', 'condition', 'action'].forEach(function (key) {
|
||||
if (!Array.isArray(config[key])) {
|
||||
config[key] = [config[key]];
|
||||
}
|
||||
});
|
||||
this.dirty = false;
|
||||
this.config = config;
|
||||
this._updateComponent();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
isWideChanged: function () {
|
||||
if (this.config === null) return;
|
||||
this._updateComponent();
|
||||
},
|
||||
|
||||
backTapped: function () {
|
||||
if (this.dirty &&
|
||||
// eslint-disable-next-line
|
||||
!confirm('You have unsaved changes. Are you sure you want to leave?')) {
|
||||
return;
|
||||
}
|
||||
this.fire('hass-automation-picked', { id: null });
|
||||
},
|
||||
|
||||
_updateComponent: function () {
|
||||
this._rendered = window.AutomationEditor(
|
||||
this.$.root, {
|
||||
automation: this.config,
|
||||
onChange: this.configChanged,
|
||||
isWide: this.isWide,
|
||||
}, this._rendered);
|
||||
},
|
||||
|
||||
saveAutomation: function () {
|
||||
this.hass.callApi(
|
||||
'post', 'config/automation/config/' + this.automation.attributes.id,
|
||||
this.config).then(function () {
|
||||
this.dirty = false;
|
||||
}.bind(this), function (errors) {
|
||||
this.errors = errors.body.message;
|
||||
throw errors;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
computeName: function (automation) {
|
||||
return automation && window.hassUtil.computeStateName(automation);
|
||||
},
|
||||
});
|
||||
</script>
|
116
panels/automation/ha-automation-picker.html
Normal file
116
panels/automation/ha-automation-picker.html
Normal file
@ -0,0 +1,116 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../bower_components/app-layout/app-header-layout/app-header-layout.html">
|
||||
<link rel="import" href="../../bower_components/app-layout/app-header/app-header.html">
|
||||
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
|
||||
<link rel="import" href="../../bower_components/paper-card/paper-card.html">
|
||||
<link rel="import" href="../../bower_components/paper-item/paper-item.html">
|
||||
<link rel="import" href="../../bower_components/paper-item/paper-item-body.html">
|
||||
|
||||
<dom-module id="ha-automation-picker">
|
||||
<template>
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
paper-card {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.content > paper-card:first-child {
|
||||
margin-bottom: 16px;
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header fixed>
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>Automations Editor</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class='content'>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
Currently Chrome is the only supported browser.
|
||||
</div>
|
||||
</paper-card>
|
||||
|
||||
<paper-card heading='Pick automation to edit'>
|
||||
<template is='dom-if' if='[[!automations.length]]'>
|
||||
<div class='card-content'>
|
||||
We couldn't find any editable automations.
|
||||
</div>
|
||||
</template>
|
||||
<template is='dom-repeat' items='[[automations]]' as='automation'>
|
||||
<paper-item>
|
||||
<paper-item-body two-line on-tap='automationTapped'>
|
||||
<div>[[computeName(automation)]]</div>
|
||||
<div secondary>[[computeDescription(automation)]]</div>
|
||||
</paper-item-body>
|
||||
[[computeStatus(automation)]]
|
||||
</paper-item>
|
||||
</template>
|
||||
</paper-card>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'ha-automation-picker',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
automations: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
|
||||
automationTapped: function (ev) {
|
||||
this.fire('hass-automation-picked', {
|
||||
id: this.automations[ev.model.index].attributes.id,
|
||||
});
|
||||
},
|
||||
|
||||
computeName: function (automation) {
|
||||
return window.hassUtil.computeStateName(automation);
|
||||
},
|
||||
|
||||
// Still thinking of something to add here.
|
||||
// eslint-disable-next-line
|
||||
computeDescription: function (automation) {
|
||||
return '';
|
||||
},
|
||||
|
||||
computeStatus: function (automation) {
|
||||
return automation.state;
|
||||
},
|
||||
});
|
||||
</script>
|
133
panels/automation/ha-panel-automation.html
Normal file
133
panels/automation/ha-panel-automation.html
Normal file
@ -0,0 +1,133 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
<link rel='import' href='../../bower_components/iron-media-query/iron-media-query.html'>
|
||||
|
||||
<link rel="import" href="./ha-automation-picker.html">
|
||||
<link rel="import" href="./ha-automation-editor.html">
|
||||
|
||||
<dom-module id="ha-panel-automation">
|
||||
<template>
|
||||
<style>
|
||||
ha-automation-picker,
|
||||
ha-automation-editor {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<iron-media-query query="(min-width: 1040px)" query-matches="{{wide}}">
|
||||
</iron-media-query>
|
||||
<iron-media-query query="(min-width: 1296px)" query-matches="{{wideSidebar}}">
|
||||
</iron-media-query>
|
||||
|
||||
<template is='dom-if' if='[[!automation]]'>
|
||||
<ha-automation-picker
|
||||
automations='[[automations]]'
|
||||
></ha-automation-picker>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[automation]]' restamp>
|
||||
<ha-automation-editor
|
||||
hass='[[hass]]'
|
||||
automation='[[automation]]'
|
||||
is-wide='[[isWide]]'
|
||||
></ha-automation-editor>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'ha-panel-automation',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
automations: {
|
||||
type: Array,
|
||||
computed: 'computeAutomations(hass)',
|
||||
},
|
||||
|
||||
automationId: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
|
||||
automation: {
|
||||
type: Object,
|
||||
computed: 'computeAutomation(automations, automationId)',
|
||||
},
|
||||
|
||||
wide: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
wideSidebar: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsWide(showMenu, wideSidebar, wide)'
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-automation-picked': 'automationPicked',
|
||||
},
|
||||
|
||||
computeIsWide: function (showMenu, wideSidebar, wide) {
|
||||
return showMenu ? wideSidebar : wide;
|
||||
},
|
||||
|
||||
computeAutomation: function (automations, automationId) {
|
||||
for (var i = 0; i < automations.length; i++) {
|
||||
if (automations[i].attributes.id === automationId) {
|
||||
return automations[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
computeAutomations: function (hass) {
|
||||
var automations = [];
|
||||
|
||||
Object.keys(hass.states).forEach(function (key) {
|
||||
var entity = hass.states[key];
|
||||
|
||||
if (window.hassUtil.computeDomain(entity) === 'automation' &&
|
||||
'id' in entity.attributes) {
|
||||
automations.push(entity);
|
||||
}
|
||||
});
|
||||
|
||||
return automations.sort(function entitySortBy(entityA, entityB) {
|
||||
var nameA = (entityA.attributes.alias ||
|
||||
entityA.entity_id).toLowerCase();
|
||||
var nameB = (entityB.attributes.alias ||
|
||||
entityB.entity_id).toLowerCase();
|
||||
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
|
||||
automationPicked: function (ev) {
|
||||
this.automationId = ev.detail.id;
|
||||
}
|
||||
});
|
||||
</script>
|
@ -7,12 +7,12 @@
|
||||
<link rel="import" href="./hassio-data.html">
|
||||
|
||||
<dom-module id="ha-panel-hassio">
|
||||
<style>
|
||||
iron-pages {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<style>
|
||||
iron-pages {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<hassio-data
|
||||
id='data'
|
||||
hass='[[hass]]'
|
||||
|
100
preact-src/automation.js
Normal file
100
preact-src/automation.js
Normal file
@ -0,0 +1,100 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import Trigger from './trigger';
|
||||
import Script from './script';
|
||||
|
||||
export default class Automation extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.triggerChanged = this.triggerChanged.bind(this);
|
||||
this.actionChanged = this.actionChanged.bind(this);
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
[ev.target.name]: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
triggerChanged(trigger) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
trigger,
|
||||
});
|
||||
}
|
||||
|
||||
actionChanged(action) {
|
||||
this.props.onChange({
|
||||
...this.props.automation,
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
render({ automation, isWide }) {
|
||||
const { alias, trigger, condition, action } = automation;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>{alias}</span>
|
||||
<span slot='introduction'>
|
||||
Use automations to bring your home alive.
|
||||
</span>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
<paper-input
|
||||
label="Name"
|
||||
name="alias"
|
||||
value={alias}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>Triggers</span>
|
||||
<span slot='introduction'>
|
||||
Like a journey, every automation starts with a single step.
|
||||
In this case it's what should trigger the automation.
|
||||
<p><a href="https://home-assistant.io/docs/automation/trigger/" target="_blank">
|
||||
Learn more about triggers.
|
||||
</a></p>
|
||||
</span>
|
||||
<Trigger trigger={trigger} onChange={this.triggerChanged} />
|
||||
</ha-config-section>
|
||||
|
||||
{ condition &&
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>Conditions</span>
|
||||
<span slot='introduction'>
|
||||
Conditions can be used to prevent an automation from executing.
|
||||
<p><a href="https://home-assistant.io/docs/scripts/conditions/" target="_blank">
|
||||
Learn more about conditions.
|
||||
</a></p>
|
||||
</span>
|
||||
<paper-card>
|
||||
<div class='card-content'>
|
||||
Conditions are not supported yet.
|
||||
<pre>{JSON.stringify(condition, null, 2)}</pre>
|
||||
</div>
|
||||
</paper-card>
|
||||
</ha-config-section>}
|
||||
|
||||
<ha-config-section is-wide={isWide}>
|
||||
<span slot='header'>Action</span>
|
||||
<span slot='introduction'>
|
||||
The action part defines what the automation should do.
|
||||
<p><a href="https://home-assistant.io/docs/scripts/" target="_blank">
|
||||
Learn more about actions.
|
||||
</a></p>
|
||||
</span>
|
||||
<Script script={action} onChange={this.actionChanged} />
|
||||
</ha-config-section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
56
preact-src/json_textarea.js
Normal file
56
preact-src/json_textarea.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
|
||||
export default class JSONTextArea extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state.isValid = true;
|
||||
this.state.value = JSON.stringify(props.value || {}, null, 2);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
const value = ev.target.value;
|
||||
let parsed;
|
||||
let isValid;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
isValid = true;
|
||||
} catch (err) {
|
||||
// Invalid JSON
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value,
|
||||
isValid,
|
||||
});
|
||||
if (isValid) {
|
||||
this.props.onChange(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ value }) {
|
||||
this.setState({
|
||||
value: JSON.stringify(value, null, 2),
|
||||
isValid: true,
|
||||
});
|
||||
}
|
||||
|
||||
render(props, { value, isValid }) {
|
||||
const style = {
|
||||
minWidth: 300,
|
||||
};
|
||||
if (!isValid) {
|
||||
style.border = '1px solid red';
|
||||
}
|
||||
return (
|
||||
<textarea
|
||||
value={value}
|
||||
style={style}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
52
preact-src/script/call_service.js
Normal file
52
preact-src/script/call_service.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import JSONTextArea from '../json_textarea';
|
||||
|
||||
export default class CallServiceAction extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.serviceDataChanged = this.serviceDataChanged.bind(this);
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.action,
|
||||
[ev.target.name]: ev.target.value
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
serviceDataChanged(service_data) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.action,
|
||||
service_data,
|
||||
});
|
||||
}
|
||||
|
||||
render({ action }) {
|
||||
const { alias, service, data } = action;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Alias"
|
||||
name="alias"
|
||||
value={alias}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Service"
|
||||
name="service"
|
||||
value={service}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Service Data<br />
|
||||
<JSONTextArea
|
||||
value={data}
|
||||
onChange={this.serviceDataChanged}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
50
preact-src/script/index.js
Normal file
50
preact-src/script/index.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import ScriptAction from './script_action';
|
||||
|
||||
export default class Script extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addAction = this.addAction.bind(this);
|
||||
this.actionChanged = this.actionChanged.bind(this);
|
||||
}
|
||||
|
||||
addAction() {
|
||||
const script = this.props.script.concat({
|
||||
service: '',
|
||||
});
|
||||
|
||||
this.props.onChange(script);
|
||||
}
|
||||
|
||||
actionChanged(index, newValue) {
|
||||
const script = this.props.script.concat();
|
||||
|
||||
if (newValue === null) {
|
||||
script.splice(index, 1);
|
||||
} else {
|
||||
script[index] = newValue;
|
||||
}
|
||||
|
||||
this.props.onChange(script);
|
||||
}
|
||||
|
||||
render({ script }) {
|
||||
return (
|
||||
<div class="script">
|
||||
{script.map((act, idx) => (
|
||||
<ScriptAction
|
||||
index={idx}
|
||||
action={act}
|
||||
onChange={this.actionChanged}
|
||||
/>))}
|
||||
<paper-card>
|
||||
<div class='card-actions add-card'>
|
||||
<paper-button onTap={this.addAction}>Add action</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
106
preact-src/script/script_action.js
Normal file
106
preact-src/script/script_action.js
Normal file
@ -0,0 +1,106 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import CallService from './call_service';
|
||||
|
||||
function getType(action) {
|
||||
if ('service' in action) {
|
||||
return 'Call Service';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const TYPES = {
|
||||
'Call Service': CallService,
|
||||
Delay: null,
|
||||
'Templated Delay': null,
|
||||
Condition: null,
|
||||
'Fire Event': null,
|
||||
};
|
||||
|
||||
const OPTIONS = Object.keys(TYPES).sort();
|
||||
|
||||
export default class Action extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.typeChanged = this.typeChanged.bind(this);
|
||||
this.onDelete = this.onDelete.bind(this);
|
||||
}
|
||||
|
||||
typeChanged(ev) {
|
||||
const newType = ev.target.selectedItem.innerHTML;
|
||||
const oldType = getType(this.props.action);
|
||||
|
||||
if (oldType !== newType) {
|
||||
this.props.onChange(this.props.index, {
|
||||
platform: newType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
// eslint-disable-next-line
|
||||
if (confirm('Sure you want to delete?')) {
|
||||
this.props.onChange(this.props.index, null);
|
||||
}
|
||||
}
|
||||
|
||||
render({ index, action, onChange }) {
|
||||
const type = getType(action);
|
||||
const Comp = TYPES[type];
|
||||
const selected = OPTIONS.indexOf(type);
|
||||
let content;
|
||||
|
||||
if (Comp) {
|
||||
content = (
|
||||
<div>
|
||||
<paper-dropdown-menu-light label="Action Type" no-animations>
|
||||
<paper-listbox
|
||||
class="dropdown-content"
|
||||
selected={selected}
|
||||
oniron-select={this.typeChanged}
|
||||
>
|
||||
{OPTIONS.map(opt => <paper-item>{opt}</paper-item>)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
<Comp
|
||||
index={index}
|
||||
action={action}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
Unsupported action
|
||||
<pre>{JSON.stringify(action, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<paper-card>
|
||||
<div class='card-menu'>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
vertical-offset="-5"
|
||||
>
|
||||
<paper-icon-button
|
||||
icon="mdi:dots-vertical"
|
||||
class="dropdown-trigger"
|
||||
/>
|
||||
<paper-menu class="dropdown-content">
|
||||
<paper-item disabled>Duplicate</paper-item>
|
||||
<paper-item onTap={this.onDelete}>Delete</paper-item>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
</div>
|
||||
<div class='card-content'>{content}</div>
|
||||
</paper-card>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
41
preact-src/trigger/event.js
Normal file
41
preact-src/trigger/event.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import JSONTextArea from '../json_textarea';
|
||||
|
||||
import { onChange } from './util';
|
||||
|
||||
export default class EventTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChange.bind(this);
|
||||
this.eventDataChanged = this.eventDataChanged.bind(this);
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
eventDataChanged(event_data) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.trigger,
|
||||
event_data,
|
||||
});
|
||||
}
|
||||
|
||||
render({ trigger }) {
|
||||
const { event_type, event_data } = trigger;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Event Type"
|
||||
name="event_type"
|
||||
value={event_type}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Event Data
|
||||
<JSONTextArea
|
||||
value={event_data}
|
||||
onChange={this.eventDataChanged}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
50
preact-src/trigger/index.js
Normal file
50
preact-src/trigger/index.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import TriggerRow from './trigger_row';
|
||||
|
||||
export default class Trigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addTrigger = this.addTrigger.bind(this);
|
||||
this.triggerChanged = this.triggerChanged.bind(this);
|
||||
}
|
||||
|
||||
addTrigger() {
|
||||
const trigger = this.props.trigger.concat({
|
||||
platform: 'event',
|
||||
});
|
||||
|
||||
this.props.onChange(trigger);
|
||||
}
|
||||
|
||||
triggerChanged(index, newValue) {
|
||||
const trigger = this.props.trigger.concat();
|
||||
|
||||
if (newValue === null) {
|
||||
trigger.splice(index, 1);
|
||||
} else {
|
||||
trigger[index] = newValue;
|
||||
}
|
||||
|
||||
this.props.onChange(trigger);
|
||||
}
|
||||
|
||||
render({ trigger }) {
|
||||
return (
|
||||
<div class="triggers">
|
||||
{trigger.map((trg, idx) => (
|
||||
<TriggerRow
|
||||
index={idx}
|
||||
trigger={trg}
|
||||
onChange={this.triggerChanged}
|
||||
/>))}
|
||||
<paper-card>
|
||||
<div class='card-actions add-card'>
|
||||
<paper-button onTap={this.addTrigger}>Add trigger</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
45
preact-src/trigger/numeric_state.js
Normal file
45
preact-src/trigger/numeric_state.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChange } from './util';
|
||||
|
||||
export default class NumericStateTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChange.bind(this);
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const { value_template, entity_id, below, above } = trigger;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Above"
|
||||
name="above"
|
||||
value={above}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="Below"
|
||||
name="below"
|
||||
value={below}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Value template (optional)<br />
|
||||
<textarea
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
style={{ width: '100%', height: 100 }}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
41
preact-src/trigger/state.js
Normal file
41
preact-src/trigger/state.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { onChange } from './util';
|
||||
|
||||
export default class StateTrigger extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChange = onChange.bind(this);
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
render({ trigger }) {
|
||||
const { entity_id, to } = trigger;
|
||||
const trgFrom = trigger.from;
|
||||
const trgFor = trigger.for;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
label="Entity Id"
|
||||
name="entity_id"
|
||||
value={entity_id}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="From"
|
||||
name="from"
|
||||
value={trgFrom}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<paper-input
|
||||
label="To"
|
||||
name="to"
|
||||
value={to}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{trgFor && <pre>For: {JSON.stringify(trgFor, null, 2)}</pre>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
103
preact-src/trigger/trigger_row.js
Normal file
103
preact-src/trigger/trigger_row.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import EventTrigger from './event';
|
||||
import StateTrigger from './state';
|
||||
import NumericStateTrigger from './numeric_state';
|
||||
|
||||
const TYPES = {
|
||||
event: EventTrigger,
|
||||
state: StateTrigger,
|
||||
homeassistant: null,
|
||||
mqtt: null,
|
||||
numeric_state: NumericStateTrigger,
|
||||
sun: null,
|
||||
template: null,
|
||||
time: null,
|
||||
zone: null,
|
||||
};
|
||||
|
||||
const OPTIONS = Object.keys(TYPES).sort();
|
||||
|
||||
export default class TriggerRow extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.typeChanged = this.typeChanged.bind(this);
|
||||
this.onDelete = this.onDelete.bind(this);
|
||||
}
|
||||
|
||||
typeChanged(ev) {
|
||||
const type = ev.target.selectedItem.innerHTML;
|
||||
|
||||
if (type !== this.props.trigger.platform) {
|
||||
this.props.onChange(this.props.index, {
|
||||
platform: type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
// eslint-disable-next-line
|
||||
if (confirm('Sure you want to delete?')) {
|
||||
this.props.onChange(this.props.index, null);
|
||||
}
|
||||
}
|
||||
|
||||
render({ index, trigger, onChange }) {
|
||||
const Comp = TYPES[trigger.platform];
|
||||
const selected = OPTIONS.indexOf(trigger.platform);
|
||||
|
||||
let content;
|
||||
|
||||
if (Comp) {
|
||||
content = (
|
||||
<div>
|
||||
<paper-dropdown-menu-light label="Trigger Type" no-animations>
|
||||
<paper-listbox
|
||||
class="dropdown-content"
|
||||
selected={selected}
|
||||
oniron-select={this.typeChanged}
|
||||
>
|
||||
{OPTIONS.map(opt => <paper-item>{opt}</paper-item>)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
<Comp
|
||||
index={index}
|
||||
trigger={trigger}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
Unsupported platform: {trigger.platform}
|
||||
<pre>{JSON.stringify(trigger, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<paper-card>
|
||||
<div class='card-menu'>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
vertical-offset="-5"
|
||||
>
|
||||
<paper-icon-button
|
||||
icon="mdi:dots-vertical"
|
||||
class="dropdown-trigger"
|
||||
/>
|
||||
<paper-menu class="dropdown-content">
|
||||
<paper-item disabled>Duplicate</paper-item>
|
||||
<paper-item onTap={this.onDelete}>Delete</paper-item>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
</div>
|
||||
<div class='card-content'>{content}</div>
|
||||
</paper-card>
|
||||
);
|
||||
}
|
||||
}
|
11
preact-src/trigger/util.js
Normal file
11
preact-src/trigger/util.js
Normal file
@ -0,0 +1,11 @@
|
||||
export function onChange(ev) {
|
||||
const trigger = { ...this.props.trigger };
|
||||
|
||||
if (ev.target.value) {
|
||||
trigger[ev.target.name] = ev.target.value;
|
||||
} else {
|
||||
delete trigger[ev.target.name];
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.index, trigger);
|
||||
}
|
3
preact-src/util.js
Normal file
3
preact-src/util.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function validEntityId(entityId) {
|
||||
return /^(\w+)\.(\w+)$/.test(entityId);
|
||||
}
|
8
rollup/automation.js
Normal file
8
rollup/automation.js
Normal file
@ -0,0 +1,8 @@
|
||||
import config from './base-config';
|
||||
|
||||
export default Object.assign({}, config, {
|
||||
entry: 'panels/automation/editor.js',
|
||||
targets: [
|
||||
{ dest: 'build/editor.js', format: 'iife' },
|
||||
],
|
||||
});
|
@ -1,14 +1,20 @@
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||
import replace from 'rollup-plugin-replace';
|
||||
import buble from 'rollup-plugin-buble';
|
||||
import babel from 'rollup-plugin-babel';
|
||||
import uglify from 'rollup-plugin-uglify';
|
||||
|
||||
const DEV = !!JSON.parse(process.env.BUILD_DEV || 'true');
|
||||
const DEMO = !!JSON.parse(process.env.BUILD_DEMO || 'false');
|
||||
|
||||
const plugins = [
|
||||
nodeResolve({}),
|
||||
babel({
|
||||
}),
|
||||
|
||||
nodeResolve({
|
||||
jsnext: true,
|
||||
main: true,
|
||||
}),
|
||||
|
||||
commonjs(),
|
||||
|
||||
@ -21,7 +27,6 @@ const plugins = [
|
||||
];
|
||||
|
||||
if (!DEV) {
|
||||
plugins.push(buble());
|
||||
plugins.push(uglify());
|
||||
}
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var fs = require('fs');
|
||||
var optimizeJs = require('optimize-js');
|
||||
|
||||
var core = fs.readFileSync('build/core.js', 'utf-8');
|
||||
core = optimizeJs(core);
|
||||
fs.writeFileSync('build/core.js', core);
|
||||
|
||||
var compatibility = fs.readFileSync('build/compatibility.js', 'utf-8');
|
||||
compatibility = optimizeJs(compatibility);
|
||||
fs.writeFileSync('build/compatibility.js', compatibility);
|
@ -1,2 +1,3 @@
|
||||
import objAssign from 'es6-object-assign';
|
||||
|
||||
objAssign.polyfill();
|
||||
|
Loading…
x
Reference in New Issue
Block a user