Implementing custom dashboard components [WIP]

This commit is contained in:
Fabio Manganiello 2021-03-27 12:26:55 +01:00
parent eb486df1ee
commit 88b788430d
25 changed files with 15558 additions and 28 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-9f884670"],{"0279":function(e,t,a){"use strict";var c=a("7a23"),n=Object(c["K"])("data-v-8fae7678");Object(c["u"])("data-v-8fae7678");var s=Object(c["h"])("div",{class:"switch"},[Object(c["h"])("div",{class:"dot"})],-1),i={class:"label"};Object(c["s"])();var o=n((function(e,t,a,n,o,l){return Object(c["r"])(),Object(c["e"])("div",{class:["power-switch",{disabled:a.disabled}],onClick:t[1]||(t[1]=function(){return l.onInput.apply(l,arguments)})},[Object(c["h"])("input",{type:"checkbox",checked:a.value},null,8,["checked"]),Object(c["h"])("label",null,[s,Object(c["h"])("span",i,[Object(c["y"])(e.$slots,"default")])])],2)})),l={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(e.stopPropagation(),this.disabled)return!1;this.$emit("input",e)}}};a("5b0a");l.render=o,l.__scopeId="data-v-8fae7678";t["a"]=l},"5b0a":function(e,t,a){"use strict";a("7ef9")},"7ef9":function(e,t,a){}}]);
//# sourceMappingURL=chunk-9f884670.9830b044.js.map
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-06539e5d"],{"0279":function(e,t,a){"use strict";var c=a("7a23"),n=Object(c["K"])("data-v-8fae7678");Object(c["u"])("data-v-8fae7678");var s=Object(c["h"])("div",{class:"switch"},[Object(c["h"])("div",{class:"dot"})],-1),i={class:"label"};Object(c["s"])();var o=n((function(e,t,a,n,o,l){return Object(c["r"])(),Object(c["e"])("div",{class:["power-switch",{disabled:a.disabled}],onClick:t[1]||(t[1]=function(){return l.onInput.apply(l,arguments)})},[Object(c["h"])("input",{type:"checkbox",checked:a.value},null,8,["checked"]),Object(c["h"])("label",null,[s,Object(c["h"])("span",i,[Object(c["y"])(e.$slots,"default")])])],2)})),l={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(e.stopPropagation(),this.disabled)return!1;this.$emit("input",e)}}};a("5b0a");l.render=o,l.__scopeId="data-v-8fae7678";t["a"]=l},"5b0a":function(e,t,a){"use strict";a("7ef9a")},"7ef9a":function(e,t,a){}}]);
//# sourceMappingURL=chunk-06539e5d.1a0f4e72.js.map

View file

@ -1,2 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-31bc5041"],{6341:function(e,n,t){"use strict";t.r(n);t("b64b");var c=t("7a23"),r=Object(c["K"])("data-v-eac2ea44");Object(c["u"])("data-v-eac2ea44");var i={class:"switches-container"},s={class:"switch-plugins"},u={key:0,class:"no-content"},a={key:0,class:"refresh col-2"},o=Object(c["h"])("i",{class:"fa fa-sync"},null,-1),l={class:"refresh-button"},d=Object(c["h"])("i",{class:"fa fa-sync"},null,-1);Object(c["s"])();var b=r((function(e,n,t,r,b,h){var f=Object(c["z"])("Loading");return Object(c["r"])(),Object(c["e"])("div",i,[b.loading?(Object(c["r"])(),Object(c["e"])(f,{key:0})):Object(c["f"])("",!0),Object(c["h"])("div",s,[Object.keys(b.plugins).length?Object(c["f"])("",!0):(Object(c["r"])(),Object(c["e"])("div",u,"No switch plugins configured")),(Object(c["r"])(!0),Object(c["e"])(c["a"],null,Object(c["x"])(Object.keys(b.plugins),(function(e){return Object(c["r"])(),Object(c["e"])("div",{class:"switch-plugin",key:e,onClick:function(n){return b.selectedPlugin=b.selectedPlugin===e?null:e}},[Object(c["h"])("div",{class:["header",{selected:b.selectedPlugin===e}]},[Object(c["h"])("div",{class:"name col-10",textContent:Object(c["C"])(e)},null,8,["textContent"]),b.selectedPlugin===e?(Object(c["r"])(),Object(c["e"])("div",a,[Object(c["h"])("button",{onClick:Object(c["J"])((function(n){return b.bus.emit("refresh",e)}),["stop"]),title:"Refresh plugin",disabled:b.loading},[o],8,["onClick","disabled"])])):Object(c["f"])("",!0)],2),Object(c["h"])("div",{class:["body",{hidden:b.selectedPlugin!==e}]},[(Object(c["r"])(),Object(c["e"])(Object(c["A"])(b.components[e]),{config:b.plugins[e],"plugin-name":e,selected:b.selectedPlugin===e,bus:b.bus},null,8,["config","plugin-name","selected","bus"]))],2)],8,["onClick"])})),128))]),Object(c["h"])("div",l,[Object(c["h"])("button",{onClick:n[1]||(n[1]=function(){return h.refresh.apply(h,arguments)}),disabled:b.loading,title:"Refresh plugins"},[d],8,["disabled"])])])})),h=(t("4160"),t("a15b"),t("d81d"),t("fb6a"),t("d3b7"),t("ac1f"),t("1276"),t("159b"),t("96cf"),t("1da1")),f=t("3a5e"),p=t("3e54"),O=t("14b7"),j={name:"Switches",components:{Loading:f["a"]},mixins:[p["a"]],data:function(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:Object(O["a"])()}},methods:{initPanels:function(){var e=this;this.components={},Object.keys(this.plugins).forEach(function(){var n=Object(h["a"])(regeneratorRuntime.mark((function n(r){var i,s,u;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return i=r.split(".").map((function(e){return e[0].toUpperCase()+e.slice(1)})).join(""),s=null,n.prev=2,n.next=5,t("c1da")("./".concat(i,"/Index"));case 5:s=n.sent,n.next=11;break;case 8:return n.prev=8,n.t0=n["catch"](2),n.abrupt("return");case 11:u=Object(c["i"])(Object(h["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",s);case 1:case"end":return e.stop()}}),e)})))),e.$options.components[r]=u,e.components[r]=u;case 14:case"end":return n.stop()}}),n,null,[[2,8]])})));return function(e){return n.apply(this,arguments)}}())},refresh:function(){var e=this;return Object(h["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.loading=!0,n.prev=1,n.next=4,e.request("utils.get_switch_plugins");case 4:e.plugins=n.sent,e.initPanels();case 6:return n.prev=6,e.loading=!1,n.finish(6);case 9:case"end":return n.stop()}}),n,null,[[1,,6,9]])})))()}},mounted:function(){this.refresh()}};t("84aa");j.render=b,j.__scopeId="data-v-eac2ea44";n["default"]=j},"7ac9":function(e,n,t){},"84aa":function(e,n,t){"use strict";t("7ac9")},c1da:function(e,n,t){var c={"./LightHue/Index":["0219","chunk-9f884670","chunk-5d632024","chunk-e017dc3e"],"./Smartthings/Index":["6e68","chunk-9f884670","chunk-5d632024","chunk-972487d6"],"./SwitchSwitchbot/Index":["5083","chunk-9f884670","chunk-5d632024","chunk-0021f7ee"],"./SwitchTplink/Index":["d11f","chunk-9f884670","chunk-5d632024","chunk-c4aee99e"],"./SwitchWemo/Index":["bedd","chunk-9f884670","chunk-5d632024","chunk-60dbbc82"],"./ZigbeeMqtt/Index":["65d6","chunk-9f884670","chunk-5d632024","chunk-07773226"],"./Zwave/Index":["e170","chunk-9f884670","chunk-5d632024","chunk-0827360a"]};function r(e){if(!t.o(c,e))return Promise.resolve().then((function(){var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}));var n=c[e],r=n[0];return Promise.all(n.slice(1).map(t.e)).then((function(){return t(r)}))}r.keys=function(){return Object.keys(c)},r.id="c1da",e.exports=r}}]);
//# sourceMappingURL=chunk-31bc5041.f4934e0d.js.map
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-31bc5041"],{6341:function(e,n,t){"use strict";t.r(n);t("b64b");var c=t("7a23"),r=Object(c["K"])("data-v-eac2ea44");Object(c["u"])("data-v-eac2ea44");var i={class:"switches-container"},s={class:"switch-plugins"},u={key:0,class:"no-content"},a={key:0,class:"refresh col-2"},o=Object(c["h"])("i",{class:"fa fa-sync"},null,-1),l={class:"refresh-button"},d=Object(c["h"])("i",{class:"fa fa-sync"},null,-1);Object(c["s"])();var b=r((function(e,n,t,r,b,h){var f=Object(c["z"])("Loading");return Object(c["r"])(),Object(c["e"])("div",i,[b.loading?(Object(c["r"])(),Object(c["e"])(f,{key:0})):Object(c["f"])("",!0),Object(c["h"])("div",s,[Object.keys(b.plugins).length?Object(c["f"])("",!0):(Object(c["r"])(),Object(c["e"])("div",u,"No switch plugins configured")),(Object(c["r"])(!0),Object(c["e"])(c["a"],null,Object(c["x"])(Object.keys(b.plugins),(function(e){return Object(c["r"])(),Object(c["e"])("div",{class:"switch-plugin",key:e,onClick:function(n){return b.selectedPlugin=b.selectedPlugin===e?null:e}},[Object(c["h"])("div",{class:["header",{selected:b.selectedPlugin===e}]},[Object(c["h"])("div",{class:"name col-10",textContent:Object(c["C"])(e)},null,8,["textContent"]),b.selectedPlugin===e?(Object(c["r"])(),Object(c["e"])("div",a,[Object(c["h"])("button",{onClick:Object(c["J"])((function(n){return b.bus.emit("refresh",e)}),["stop"]),title:"Refresh plugin",disabled:b.loading},[o],8,["onClick","disabled"])])):Object(c["f"])("",!0)],2),Object(c["h"])("div",{class:["body",{hidden:b.selectedPlugin!==e}]},[(Object(c["r"])(),Object(c["e"])(Object(c["A"])(b.components[e]),{config:b.plugins[e],"plugin-name":e,selected:b.selectedPlugin===e,bus:b.bus},null,8,["config","plugin-name","selected","bus"]))],2)],8,["onClick"])})),128))]),Object(c["h"])("div",l,[Object(c["h"])("button",{onClick:n[1]||(n[1]=function(){return h.refresh.apply(h,arguments)}),disabled:b.loading,title:"Refresh plugins"},[d],8,["disabled"])])])})),h=(t("4160"),t("a15b"),t("d81d"),t("fb6a"),t("d3b7"),t("ac1f"),t("1276"),t("159b"),t("96cf"),t("1da1")),f=t("3a5e"),p=t("3e54"),O=t("14b7"),j={name:"Switches",components:{Loading:f["a"]},mixins:[p["a"]],data:function(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:Object(O["a"])()}},methods:{initPanels:function(){var e=this;this.components={},Object.keys(this.plugins).forEach(function(){var n=Object(h["a"])(regeneratorRuntime.mark((function n(r){var i,s,u;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return i=r.split(".").map((function(e){return e[0].toUpperCase()+e.slice(1)})).join(""),s=null,n.prev=2,n.next=5,t("c1da")("./".concat(i,"/Index"));case 5:s=n.sent,n.next=11;break;case 8:return n.prev=8,n.t0=n["catch"](2),n.abrupt("return");case 11:u=Object(c["i"])(Object(h["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",s);case 1:case"end":return e.stop()}}),e)})))),e.$options.components[r]=u,e.components[r]=u;case 14:case"end":return n.stop()}}),n,null,[[2,8]])})));return function(e){return n.apply(this,arguments)}}())},refresh:function(){var e=this;return Object(h["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.loading=!0,n.prev=1,n.next=4,e.request("utils.get_switch_plugins");case 4:e.plugins=n.sent,e.initPanels();case 6:return n.prev=6,e.loading=!1,n.finish(6);case 9:case"end":return n.stop()}}),n,null,[[1,,6,9]])})))()}},mounted:function(){this.refresh()}};t("84aa");j.render=b,j.__scopeId="data-v-eac2ea44";n["default"]=j},"7ac9":function(e,n,t){},"84aa":function(e,n,t){"use strict";t("7ac9")},c1da:function(e,n,t){var c={"./LightHue/Index":["0219","chunk-06539e5d","chunk-5d632024","chunk-e017dc3e"],"./Smartthings/Index":["6e68","chunk-06539e5d","chunk-5d632024","chunk-972487d6"],"./SwitchSwitchbot/Index":["5083","chunk-06539e5d","chunk-5d632024","chunk-0021f7ee"],"./SwitchTplink/Index":["d11f","chunk-06539e5d","chunk-5d632024","chunk-c4aee99e"],"./SwitchWemo/Index":["bedd","chunk-06539e5d","chunk-5d632024","chunk-60dbbc82"],"./ZigbeeMqtt/Index":["65d6","chunk-06539e5d","chunk-5d632024","chunk-07773226"],"./Zwave/Index":["e170","chunk-06539e5d","chunk-5d632024","chunk-0827360a"]};function r(e){if(!t.o(c,e))return Promise.resolve().then((function(){var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}));var n=c[e],r=n[0];return Promise.all(n.slice(1).map(t.e)).then((function(){return t(r)}))}r.keys=function(){return Object.keys(c)},r.id="c1da",e.exports=r}}]);
//# sourceMappingURL=chunk-31bc5041.821f5281.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -67,6 +67,8 @@ export default {
for (let handler of handlers) {
if (handler instanceof Array)
handler = handler[0]
else if (handler instanceof Object)
handler = Object.values(handler)[0]
handler(event.args)
}

View file

@ -0,0 +1,210 @@
<template>
<div class="component-widget">
<Loading v-if="loading" />
<div class="container" ref="container" />
</div>
</template>
<script>
import Utils from "@/Utils";
import Loading from "@/components/Loading";
import components from './index'
import { createApp, h } from "vue";
import mitt from 'mitt';
const bus = mitt();
export default {
name: "Elements",
components: {Loading},
mixins: [Utils],
props: {
content: {
type: String,
},
},
data() {
return {
loading: false,
unwatch: null,
}
},
methods: {
_parseActions(element) {
const actionsTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'actions')
const children = actionsTags?.length ? actionsTags[0].children : element.children
const actionTags = [...children].filter((node) => node.tagName?.toLowerCase() === 'action')
if (!actionTags?.length)
return
return [...actionTags]
.map((actionTag) => {
return {
action: actionTag.attributes.name.value,
args: [...actionTag.children].reduce((obj, arg) => {
let value = undefined
try {
value = JSON.parse(arg.innerText)
} catch (e) {
if (arg.innerText?.length)
value = arg.innerText
}
obj[arg.tagName.toLowerCase()] = value
return obj
}, {}),
}
})
},
_parseVars(element) {
const varsTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'vars')
if (!varsTags?.length)
return
return [...varsTags[0].children].reduce((vars, varTag) => {
let value = undefined
try {
value = JSON.parse(varTag.innerText)
} catch (e) {
if (varTag.innerText?.length)
value = varTag.innerText
}
vars[varTag.tagName.toLowerCase()] = value
return vars
}, {})
},
_parseHandlers(element) {
const handlers = {}
const parseHndlScript = (hndlText) => {
return (app) => {
return eval(`// noinspection JSUnusedLocalSymbols
(async function (self) {
${hndlText}
})`)(app)
}
}
const parseEventHndl = (hndlText) => {
return (app) => {
return (event) => {
return eval(`// noinspection JSUnusedLocalSymbols
(async function (self, event) {
${hndlText}
})`)(app, event)
}
}
}
const hndlTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'handlers')
if (hndlTags?.length) {
const mounted = [...hndlTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'mounted')
if (mounted?.length)
handlers.mounted = parseHndlScript(mounted[0].innerText)
const refresh = [...hndlTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'refresh')
if (refresh?.length) {
handlers.refresh = {
handler: parseHndlScript(refresh[0].innerText),
interval: refresh[0].attributes.interval?.value || 10,
}
}
const events = [...hndlTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'event')
if (events?.length)
handlers.events = events.reduce((events, hndlTag) => {
events[hndlTag.attributes.type.value] = parseEventHndl(hndlTag.innerText)
return events
}, {})
}
const actionsTags = [...element.children].filter((node) => node.tagName?.toLowerCase() === 'actions')
if (actionsTags?.length) {
const beforeActionsTags = [...actionsTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'before')
if (beforeActionsTags?.length)
handlers.beforeActions = parseHndlScript(beforeActionsTags[0].innerText)
const afterActionsTags = [...actionsTags[0].children].filter((node) => node.tagName?.toLowerCase() === 'after')
if (afterActionsTags?.length)
handlers.afterActions = parseHndlScript(afterActionsTags[0].innerText)
}
return handlers
},
_parseProps(element) {
return [...element.attributes].reduce((obj, attr) => {
obj[attr.name] = attr.value
return obj
}, {})
},
propagateEvent(event) {
bus.emit('event', event)
},
_addEventHandler() {
this.unwatch = this.subscribe((event) => {
bus.emit('event', event)
})
},
_removeEventHandler() {
if (this.unwatch) {
this.unwatch()
this.unwatch = null
}
},
},
mounted() {
this.loading = true
this._addEventHandler()
try {
this.$refs.container.innerHTML = this.content
Object.entries(components).forEach(([name, component]) => {
this.$options.components[name] = component;
[...this.$refs.container.getElementsByTagName(name)].forEach((element) => {
const props = this._parseProps(element)
props.actions = this._parseActions(element)
props.handlers = this._parseHandlers(element)
props._vars = this._parseVars(element)
createApp({
render() { return h(component, props) },
data() {
return { bus: bus }
},
}).mount(element)
})
})
for (const tagName of ['handlers', 'actions', 'vars'])
this.$refs.container.getElementsByTagName(tagName).forEach((hndlTag) => {
hndlTag.parentNode.removeChild(hndlTag)
})
} finally {
this.loading = false
}
},
unmounted() {
this._removeEventHandler()
},
}
</script>
<style lang="scss" scoped>
.component-widget {
margin: -.75em 0 0 -.75em !important;
padding: 0;
width: calc(100% + 1.5em);
height: calc(100% + 1.5em);
}
</style>

View file

@ -0,0 +1,79 @@
<template>
<div class="run component-row" @click="run">
<div :class="{'col-10': hasIcon, 'col-12': !hasIcon}" v-text="name" />
<div class="col-2 icon-container" v-if="hasIcon">
<img class="icon" :src="iconUrl" :alt="name" v-if="iconUrl?.length">
<i class="icon" :class="iconClass" :style="iconStyle" v-else />
</div>
</div>
</template>
<script>
import mixins from './mixins';
/**
* This component is used to run one or more actions.
*/
export default {
name: "Run",
mixins: [mixins],
props: {
/**
* Component name
*/
name: {
type: String,
default: '[Unnamed action]',
},
/**
* Action (FontAwesome) icon class (default: `fa fa-play`)
*/
iconClass: {
type: String,
},
/**
* Action icon URL (default: `fa fa-play`)
*/
iconUrl: {
type: String,
},
/**
* Action icon color override, for FontAwesome icons
*/
iconColor: {
type: String,
},
},
computed: {
iconStyle() {
if (!this.iconClass?.length && this.iconColor?.length)
return
return {'color': this.iconColor}
},
hasIcon() {
return this.iconUrl?.length || this.iconClass?.length
},
}
}
</script>
<style lang="scss" scoped>
@import "mixins";
.run {
.icon-container {
position: relative;
.icon {
position: absolute;
right: 0;
}
}
}
</style>

View file

@ -0,0 +1,85 @@
<template>
<div class="slider-root component-row">
<div class="col-7" v-text="name" />
<div class="col-5 slider-container">
<div class="slider">
<SliderElement :value="value" :range="[parseFloat(min), parseFloat(max)]" @mouseup="run" />
</div>
</div>
</div>
</template>
<script>
import mixins from './mixins';
import SliderElement from "@/components/elements/Slider";
/**
* This component can be used to run action on the basis of a
* numeric value included in a specified interval (i.e. a slider).
*/
export default {
name: "Slider",
components: {SliderElement},
mixins: [mixins],
props: {
/**
* Display name for this slider.
*/
name: {
type: String,
default: '[Unnamed slider]',
},
/**
* Minimum value for the slider (default: 0).
*/
min: {
type: [String, Number],
default: 0,
},
/**
* Maximum value for the slider.
*/
max: {
type: [String, Number],
required: true,
},
},
methods: {
async run(event) {
this.value = parseFloat(event.target.value)
if (this.handlers.beforeActions)
await this.handlers.beforeActions(this)
for (const action of this.actions)
await this.request_(action)
if (this.handlers.afterActions) {
await this.handlers.afterActions(this)
}
},
},
data() {
return {
value: undefined,
}
},
}
</script>
<style lang="scss" scoped>
@import "mixins";
.slider-root {
.slider-container {
position: relative;
.slider {
position: absolute;
right: 0;
}
}
}
</style>

View file

@ -0,0 +1,55 @@
<template>
<div class="switch component-row" @click="run">
<div class="col-10" v-text="name" />
<div class="col-2 toggle-container">
<div class="toggle">
<ToggleSwitch :value="value" @input.stop="run" />
</div>
</div>
</div>
</template>
<script>
import mixins from './mixins';
import ToggleSwitch from "@/components/elements/ToggleSwitch";
/**
* This component can be used to trigger toggle actions on
* entities with a binary (ON/OFF) state.
*/
export default {
name: "Switch",
components: {ToggleSwitch},
mixins: [mixins],
props: {
/**
* Display name for this switch.
*/
name: {
type: String,
default: '[Unnamed switch]',
},
},
data() {
return {
value: undefined,
}
},
}
</script>
<style lang="scss" scoped>
@import "mixins";
.switch {
.toggle-container {
position: relative;
.toggle {
position: absolute;
right: 0;
}
}
}
</style>

View file

@ -0,0 +1,132 @@
import Utils from "@/Utils";
export default {
mixins: [Utils],
props: {
// Actions to run upon interaction with the widget. Format:
//
// [
// {
// "action": "light.hue.toggle",
// "args": {
// "lights": ["Bulb 1", "Bulb 2"]
// }
// },
// {
// "action": "music.mpd.pause"
// }
// ]
actions: {
type: Array,
default: () => { return [] },
},
// Map of variables used by this component, in the form
// of variable_name -> variable_value.
_vars: {
type: Object,
default: () => { return {} },
},
// Map of handlers, in the form of event_type -> functions.
// Supported event handler types:
//
// - mounted: Function to execute when the component is mounted.
// - beforeActions: Function to execute before the component action is run.
// - afterActions: Function to execute after the component action is run.
// - refresh: Function to be called at startup (if mounted is also specified
// then refresh will be called after mounted when the component is
// first mounted) and at regular intervals defined on the
// interval property (default: 10 seconds).
// - events: This is a mapping of functions that react to Platypush
// platform events published on the websocket (e.g. lights or
// switches toggles, media events etc.). The form is
// platypush_event_type -> function.
handlers: {
type: Object,
default: () => { return {} },
},
// Event bus
bus: {
type: Object,
},
},
data() {
return {
vars: {...(this._vars || {})},
_interval: undefined,
refresh: null,
refreshInterval: null,
value: null,
}
},
methods: {
async run() {
if (this.handlers.input)
return this.handlers.input(this)(this.value)
if (this.handlers.beforeActions)
await this.handlers.beforeActions(this)
for (const action of this.actions)
await this.request_(action)
if (this.handlers.afterActions) {
await this.handlers.afterActions(this)
}
},
async request_(action) {
const args = Object.entries(action.args).reduce((args, [key, value]) => {
if (value.trim) {
value = value.trim()
const m = value.match(/^{{\s*(.*)\s*}}/)
if (m) {
value = eval(`// noinspection JSUnusedLocalSymbols
(function (self) {
return ${m[1]}
})`)(this)
}
}
args[key] = value
return args
}, {})
await this.request(action.action, args)
},
async processEvent(event) {
const hndl = (this.handlers.events || {})[event.type]
if (hndl)
await hndl(this)(event)
},
},
async mounted() {
this.$root.bus.on('event', this.processEvent)
if (this.handlers.mounted)
await this.handlers.mounted(this)
if (this.handlers.refresh) {
this.refreshInterval = (this.handlers.refresh?.interval || 0) * 1000
this.refresh = () => {
this.handlers.refresh.handler(this)
}
await this.refresh()
if (this.refreshInterval) {
const self = this
const wrapper = () => { return self.refresh() }
this._interval = setInterval(wrapper, this.refreshInterval)
}
}
},
unmounted() {
if (this._interval)
clearInterval(this._interval)
}
}

View file

@ -0,0 +1,15 @@
@mixin component-row {
width: 100%;
display: flex;
cursor: pointer;
padding: .75em .5em;
border-bottom: $default-border;
&:hover {
background: $hover-bg;
}
}
.component-row {
@include component-row;
}

View file

@ -0,0 +1,9 @@
import Run from './components/Run'
import Switch from './components/Switch'
import Slider from './components/Slider'
export default {
Run,
Switch,
Slider,
}

View file

@ -40,7 +40,7 @@ export default {
},
generateId() {
return btoa([...Array(16).keys()].forEach(() => String.fromCharCode(Math.round(Math.random() * 255))))
return btoa([...Array(11).keys()].map(() => String.fromCharCode(Math.round(Math.random() * 255))))
},
}
}

View file

@ -59,27 +59,29 @@ export default {
parseTemplate(name, tmpl) {
const node = new DOMParser().parseFromString(tmpl, 'text/xml').childNodes[0]
const self = this
this.style = node.attributes.style ? node.attributes.style.nodeValue : undefined
this.class = node.attributes.class ? node.attributes.class.nodeValue : undefined
this.style = node.attributes.style?.nodeValue
this.class = node.attributes.class?.nodeValue
this.rows = [...node.getElementsByTagName('Row')].map((row) => {
return {
style: row.attributes.style ? row.attributes.style.nodeValue : undefined,
class: row.attributes.class ? row.attributes.class.nodeValue : undefined,
style: row.attributes.style?.nodeValue,
class: row.attributes.class?.nodeValue,
widgets: [...row.children].map((el) => {
const component = defineAsyncComponent(
() => import(`@/components/widgets/${el.nodeName}/Index`)
)
const style = el.attributes.style ? el.attributes.style.nodeValue : undefined
const classes = el.attributes.class ? el.attributes.class.nodeValue : undefined
const style = el.attributes.style?.nodeValue
const classes = el.attributes.class?.nodeValue
const attrs = [...el.attributes].reduce((obj, node) => {
if (node.nodeName !== 'style') {
obj[node.nodeName] = node.nodeValue
}
return obj
}, {})
}, {
content: el.innerHTML,
})
const widget = {
component: component,