Added support for custom dashboard components [see #129]

This commit is contained in:
Fabio Manganiello 2021-03-27 12:26:55 +01:00
parent eb486df1ee
commit 177c697f83
27 changed files with 15612 additions and 28 deletions

View file

@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2. Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
## [Unreleased]
### Added
- Added support for custom dashboard widgets with customized (see https://git.platypush.tech/platypush/platypush/-/wikis/Backends#creating-custom-widgets).
## [0.20.7] - 2021-03-26 ## [0.20.7] - 2021-03-26
### Fixed ### Fixed

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){}}]); (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-9f884670.9830b044.js.map //# 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}}]); (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.f4934e0d.js.map //# 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) { for (let handler of handlers) {
if (handler instanceof Array) if (handler instanceof Array)
handler = handler[0] handler = handler[0]
else if (handler instanceof Object)
handler = Object.values(handler)[0]
handler(event.args) 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,25 @@
<template>
<div class="run component-row" @click="run">
<div class="col-1 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 :class="{'col-11': hasIcon, 'col-12': !hasIcon}" v-text="name" />
</div>
</template>
<script>
import mixins from './mixins';
/**
* This component is used to run one or more actions.
*/
export default {
name: "Run",
mixins: [mixins],
}
</script>
<style lang="scss" scoped>
@import "mixins";
</style>

View file

@ -0,0 +1,68 @@
<template>
<div class="sensor component-row" @click="run">
<div class="col-1 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 :class="{'col-8': hasIcon, 'col-9': !hasIcon}" v-text="name" />
<div class="col-3 value-container">
<div class="value">
{{ value }}
<span v-if="unit" v-text="unit" />
</div>
</div>
</div>
</template>
<script>
import mixins from './mixins';
/**
* This component is used to monitor values from sensors.
*/
export default {
name: "Sensor",
mixins: [mixins],
props: {
/**
* Optional unit used for the sensor value
*/
unit: {
type: String,
},
},
methods: {
async run() {
if (this.handlers.beforeActions)
await this.handlers.beforeActions(this)
if (this.actions?.length)
for (const action of this.actions)
await this.request_(action)
else
await this.refresh()
if (this.handlers.afterActions) {
await this.handlers.afterActions(this)
}
},
}
}
</script>
<style lang="scss" scoped>
@import "mixins";
.sensor {
.value-container {
position: relative;
.value {
position: absolute;
right: 0;
font-weight: bold;
}
}
}
</style>

View file

@ -0,0 +1,75 @@
<template>
<div class="slider-root component-row">
<div class="col-1 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 :class="{'col-6': hasIcon, 'col-7': !hasIcon}" 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: {
/**
* 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)
}
},
},
}
</script>
<style lang="scss" scoped>
@import "mixins";
.slider-root {
.slider-container {
position: relative;
.slider {
position: absolute;
right: 0;
}
}
}
</style>

View file

@ -0,0 +1,44 @@
<template>
<div class="switch component-row" @click="run">
<div class="col-1 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 :class="{'col-9': hasIcon, 'col-10': !hasIcon}" 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],
}
</script>
<style lang="scss" scoped>
@import "mixins";
.switch {
.toggle-container {
position: relative;
.toggle {
position: absolute;
right: 0;
}
}
}
</style>

View file

@ -0,0 +1,183 @@
import Utils from "@/Utils";
export default {
mixins: [Utils],
props: {
/**
* Component name
*/
name: {
type: String,
default: '[Unnamed sensor]',
},
/**
* 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,
},
/**
* 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
* 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,
loading: false,
}
},
computed: {
iconStyle() {
if (!this.iconClass?.length && this.iconColor?.length)
return
return {'color': this.iconColor}
},
hasIcon() {
return this.iconUrl?.length || this.iconClass?.length
},
},
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,11 @@
import Run from './components/Run'
import Switch from './components/Switch'
import Slider from './components/Slider'
import Sensor from "@/components/widgets/Component/components/Sensor";
export default {
Run,
Switch,
Slider,
Sensor,
}

View file

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