Added zwave.mqtt plugin and backend [closes #186]

This commit is contained in:
Fabio Manganiello 2021-04-12 02:45:59 +02:00
parent 75e1f35523
commit c006c4b368
44 changed files with 2798 additions and 790 deletions

View file

@ -5,6 +5,10 @@ Given the high speed of development in the first phase, changes are being report
## [Unreleased] ## [Unreleased]
### Added
- Added zwavejs2mqtt integration (see [#186](https://git.platypush.tech/platypush/platypush/-/issues/186).
### Fixed ### Fixed
- Major LINT fixes. - Major LINT fixes.

View file

@ -80,3 +80,4 @@ Backends
platypush/backend/wiimote.rst platypush/backend/wiimote.rst
platypush/backend/zigbee.mqtt.rst platypush/backend/zigbee.mqtt.rst
platypush/backend/zwave.rst platypush/backend/zwave.rst
platypush/backend/zwave.mqtt.rst

View file

@ -0,0 +1,5 @@
``platypush.backend.zwave.mqtt``
================================
.. automodule:: platypush.backend.zwave.mqtt
:members:

View file

@ -0,0 +1,5 @@
``platypush.plugins.zwave.mqtt``
================================
.. automodule:: platypush.plugins.zwave.mqtt
:members:

View file

@ -145,3 +145,4 @@ Plugins
platypush/plugins/zeroconf.rst platypush/plugins/zeroconf.rst
platypush/plugins/zigbee.mqtt.rst platypush/plugins/zigbee.mqtt.rst
platypush/plugins/zwave.rst platypush/plugins/zwave.rst
platypush/plugins/zwave.mqtt.rst

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0ac54d"],{"18a1":function(e,n,a){"use strict";a.r(n);var c=a("7a23");function t(e,n,a,t,r,w){var o=Object(c["z"])("Zwave");return Object(c["r"])(),Object(c["e"])(o,{"plugin-name":"zwave.mqtt"})}var r=a("8fec"),w={components:{Zwave:r["a"]}};w.render=t;n["default"]=w}}]);
//# sourceMappingURL=chunk-2d0ac54d.74f539e1.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/ZwaveMqtt/Index.vue","webpack:///./src/components/panels/ZwaveMqtt/Index.vue?4c19"],"names":["plugin-name","components","Zwave","render"],"mappings":"uNACE,eAAkC,GAA3BA,cAAY,e,gBAMN,GACbC,WAAY,CAACC,QAAA,OCLf,EAAOC,OAASA,EAED","file":"static/js/chunk-2d0ac54d.74f539e1.js","sourcesContent":["<template>\n <Zwave plugin-name=\"zwave.mqtt\" />\n</template>\n\n<script>\nimport Zwave from \"@/components/panels/Zwave/Zwave\";\n\nexport default {\n components: {Zwave},\n}\n</script>\n","import { render } from \"./Index.vue?vue&type=template&id=8fb9cbb2\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0b21a7"],{"234d":function(e,n,a){"use strict";a.r(n);var c=a("7a23");function r(e,n,a,r,t,w){var o=Object(c["z"])("Zwave");return Object(c["r"])(),Object(c["e"])(o,{"plugin-name":"zwave"})}var t=a("8fec"),w={components:{Zwave:t["a"]}};w.render=r;n["default"]=w}}]);
//# sourceMappingURL=chunk-2d0b21a7.4a00e73b.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Zwave/Index.vue","webpack:///./src/components/panels/Zwave/Index.vue?6adc"],"names":["plugin-name","components","Zwave","render"],"mappings":"uNACE,eAA6B,GAAtBA,cAAY,U,gBAMN,GACbC,WAAY,CAACC,QAAA,OCLf,EAAOC,OAASA,EAED","file":"static/js/chunk-2d0b21a7.4a00e73b.js","sourcesContent":["<template>\n <Zwave plugin-name=\"zwave\" />\n</template>\n\n<script>\nimport Zwave from \"@/components/panels/Zwave/Zwave\";\n\nexport default {\n components: {Zwave},\n}\n</script>\n","import { render } from \"./Index.vue?vue&type=template&id=4b554bd5\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +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"},u={class:"switch-plugins"},s={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 h=r((function(e,n,t,r,h,b){var f=Object(c["z"])("Loading");return Object(c["r"])(),Object(c["e"])("div",i,[h.loading?(Object(c["r"])(),Object(c["e"])(f,{key:0})):Object(c["f"])("",!0),Object(c["h"])("div",u,[Object.keys(h.plugins).length?Object(c["f"])("",!0):(Object(c["r"])(),Object(c["e"])("div",s,"No switch plugins configured")),(Object(c["r"])(!0),Object(c["e"])(c["a"],null,Object(c["x"])(Object.keys(h.plugins),(function(e){return Object(c["r"])(),Object(c["e"])("div",{class:"switch-plugin",key:e,onClick:function(n){return h.selectedPlugin=h.selectedPlugin===e?null:e}},[Object(c["h"])("div",{class:["header",{selected:h.selectedPlugin===e}]},[Object(c["h"])("div",{class:"name col-10",textContent:Object(c["C"])(e)},null,8,["textContent"]),h.selectedPlugin===e?(Object(c["r"])(),Object(c["e"])("div",a,[Object(c["h"])("button",{onClick:Object(c["J"])((function(n){return h.bus.emit("refresh",e)}),["stop"]),title:"Refresh plugin",disabled:h.loading},[o],8,["onClick","disabled"])])):Object(c["f"])("",!0)],2),Object(c["h"])("div",{class:["body",{hidden:h.selectedPlugin!==e}]},[(Object(c["r"])(),Object(c["e"])(Object(c["A"])(h.components[e]),{config:h.plugins[e],"plugin-name":e,selected:h.selectedPlugin===e,bus:h.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 b.refresh.apply(b,arguments)}),disabled:h.loading,title:"Refresh plugins"},[d],8,["disabled"])])])})),b=(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"),k=t("14b7"),O={name:"Switches",components:{Loading:f["a"]},mixins:[p["a"]],data:function(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:Object(k["a"])()}},methods:{initPanels:function(){var e=this;this.components={},Object.keys(this.plugins).forEach(function(){var n=Object(b["a"])(regeneratorRuntime.mark((function n(r){var i,u,s;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(""),u=null,n.prev=2,n.next=5,t("c1da")("./".concat(i,"/Index"));case 5:u=n.sent,n.next=11;break;case 8:return n.prev=8,n.t0=n["catch"](2),n.abrupt("return");case 11:s=Object(c["i"])(Object(b["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",u);case 1:case"end":return e.stop()}}),e)})))),e.$options.components[r]=s,e.components[r]=s;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(b["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");O.render=h,O.__scopeId="data-v-eac2ea44";n["default"]=O},"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-35986630"],"./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"],"./ZwaveMqtt/Index":["8b26","chunk-06539e5d","chunk-5d632024","chunk-41adab28"]};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.fa7aa55d.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
(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-35986630"],"./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.ff5b04fa.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-41adab28"],{"1e64":function(e,t,c){},"8b26":function(e,t,c){"use strict";c.r(t);c("b64b");var n=c("7a23"),a=Object(n["K"])("data-v-c92e52f8");Object(n["u"])("data-v-c92e52f8");var i={class:"switches zwave-mqtt-switches"},o={key:1,class:"no-content"};Object(n["s"])();var d=a((function(e,t,c,a,d,s){var b=Object(n["z"])("Loading"),r=Object(n["z"])("Switch");return Object(n["r"])(),Object(n["e"])("div",i,[e.loading?(Object(n["r"])(),Object(n["e"])(b,{key:0})):Object.keys(e.devices).length?Object(n["f"])("",!0):(Object(n["r"])(),Object(n["e"])("div",o,"No Z-Wave switches found.")),(Object(n["r"])(!0),Object(n["e"])(n["a"],null,Object(n["x"])(e.devices,(function(t,c){return Object(n["r"])(),Object(n["e"])(r,{loading:e.loading,name:c,state:t.on,id:t.id,onToggle:function(n){return e.toggle(c,t.id)},key:c},null,8,["loading","name","state","id","onToggle"])})),128))])})),s=c("3a5e"),b=c("487b"),r=c("17dc"),j={name:"ZwaveMqtt",components:{Switch:r["a"],Loading:s["a"]},mixins:[b["a"]]};c("e64d");j.render=d,j.__scopeId="data-v-c92e52f8";t["default"]=j},e64d:function(e,t,c){"use strict";c("1e64")}}]);
//# sourceMappingURL=chunk-41adab28.8ac6033c.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Switches/ZwaveMqtt/Index.vue","webpack:///./src/components/panels/Switches/ZwaveMqtt/Index.vue?d592","webpack:///./src/components/panels/Switches/ZwaveMqtt/Index.vue?b4aa"],"names":["class","loading","Object","keys","devices","length","device","name","state","on","id","toggle","key","components","Switch","Loading","mixins","render","__scopeId"],"mappings":"yPACOA,MAAM,gC,SAEJA,MAAM,c,mIAFb,eAMM,MANN,EAMM,CALW,EAAAC,S,iBAAf,eAA0B,YACUC,OAAOC,KAAK,EAAAC,SAASC,O,wCAAzD,eAAgG,MAAhG,EAAiE,+B,mBAEjE,eACwD,2BAAvB,EAAAD,SAAO,SAAxBE,EAAQC,G,wBADxB,eACwD,GAD/CN,QAAS,EAAAA,QAAUM,KAAMA,EAAOC,MAAOF,EAAOG,GAAKC,GAAIJ,EAAOI,GAAK,SAAM,mBAAE,EAAAC,OAAOJ,EAAMD,EAAOI,KAC7DE,IAAKL,G,qGASrC,GACbA,KAAM,YACNM,WAAY,CAACC,SAAA,KAAQC,UAAA,MACrBC,OAAQ,CAAC,S,UCbX,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ,gB,kCCRf","file":"static/js/chunk-41adab28.8ac6033c.js","sourcesContent":["<template>\n <div class=\"switches zwave-mqtt-switches\">\n <Loading v-if=\"loading\" />\n <div class=\"no-content\" v-else-if=\"!Object.keys(devices).length\">No Z-Wave switches found.</div>\n\n <Switch :loading=\"loading\" :name=\"name\" :state=\"device.on\" :id=\"device.id\" @toggle=\"toggle(name, device.id)\"\n v-for=\"(device, name) in devices\" :key=\"name\" />\n </div>\n</template>\n\n<script>\nimport Loading from \"@/components/Loading\";\nimport SwitchMixin from \"@/components/panels/Switches/Mixin\";\nimport Switch from \"@/components/panels/Switches/Switch\";\n\nexport default {\n name: \"ZwaveMqtt\",\n components: {Switch, Loading},\n mixins: [SwitchMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"../common\";\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=c92e52f8&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=c92e52f8&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-c92e52f8\"\n\nexport default script","export * from \"-!../../../../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--8-oneOf-1-0!../../../../../node_modules/css-loader/dist/cjs.js??ref--8-oneOf-1-1!../../../../../node_modules/vue-loader-v16/dist/stylePostLoader.js!../../../../../node_modules/postcss-loader/src/index.js??ref--8-oneOf-1-2!../../../../../node_modules/sass-loader/dist/cjs.js??ref--8-oneOf-1-3!../../../../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../../../../node_modules/vue-loader-v16/dist/index.js??ref--0-1!./Index.vue?vue&type=style&index=0&id=c92e52f8&lang=scss&scoped=true\""],"sourceRoot":""}

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

@ -68,6 +68,9 @@
}, },
"zwave": { "zwave": {
"imgUrl": "/icons/z-wave.png" "imgUrl": "/icons/z-wave.png"
},
"zwave.mqtt": {
"imgUrl": "/icons/z-wave.png"
} }
} }
} }

View file

@ -0,0 +1,25 @@
<template>
<div class="switches zwave-mqtt-switches">
<Loading v-if="loading" />
<div class="no-content" v-else-if="!Object.keys(devices).length">No Z-Wave switches found.</div>
<Switch :loading="loading" :name="name" :state="device.on" :id="device.id" @toggle="toggle(name, device.id)"
v-for="(device, name) in devices" :key="name" />
</div>
</template>
<script>
import Loading from "@/components/Loading";
import SwitchMixin from "@/components/panels/Switches/Mixin";
import Switch from "@/components/panels/Switches/Switch";
export default {
name: "ZwaveMqtt",
components: {Switch, Loading},
mixins: [SwitchMixin],
}
</script>
<style lang="scss" scoped>
@import "../common";
</style>

View file

@ -4,6 +4,16 @@
@click="$emit('select', group.index)" /> @click="$emit('select', group.index)" />
<div class="params" v-if="selected"> <div class="params" v-if="selected">
<div class="section owner" v-if="owner && Object.keys(owner).length">
<div class="header">
<div class="title">Owner</div>
</div>
<div class="body">
<div class="row" v-text="owner.name" />
</div>
</div>
<div class="section nodes"> <div class="section nodes">
<div class="header"> <div class="header">
<div class="title col-10">Nodes</div> <div class="title col-10">Nodes</div>
@ -50,18 +60,21 @@
</template> </template>
<script> <script>
import Utils from "@/Utils"; import mixin from "@/components/panels/Zwave/mixin";
export default { export default {
name: "Group", name: "Group",
emits: ['select', 'open-add-nodes-to-group'], emits: ['select', 'open-add-nodes-to-group'],
mixins: [Utils], mixins: [mixin],
props: { props: {
group: { group: {
type: Object, type: Object,
required: true, required: true,
}, },
owner: {
type: Object,
},
nodes: { nodes: {
type: Object, type: Object,
default: () => { return {} }, default: () => { return {} },
@ -84,11 +97,17 @@ export default {
return return
this.commandRunning = true this.commandRunning = true
try { const args = {
await this.request('zwave.remove_node_from_group', {
node_id: nodeId, node_id: nodeId,
group_index: this.group.index, }
})
if (this.group.group_id != null)
args.group_id = this.group.group_id
else
args.group_index = this.group.index
try {
await this.zrequest('remove_node_from_group', args)
} finally { } finally {
this.commandRunning = false this.commandRunning = false
} }
@ -99,4 +118,17 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "common"; @import "common";
.section.nodes {
.header, .row {
position: relative;
.buttons {
position: absolute;
right: 0;
display: flex;
justify-content: right;
}
}
}
</style> </style>

View file

@ -1,738 +1,11 @@
<template> <template>
<div class="zwave-container"> <Zwave plugin-name="zwave" />
<Modal title="Network info" ref="networkInfoModal">
<div class="network-info">
<Loading v-if="loading.status" />
<div class="params" v-else>
<div class="row">
<div class="param-name">State</div>
<div class="param-value" v-text="status.state"></div>
</div>
<div class="row">
<div class="param-name">Device</div>
<div class="param-value" v-text="status.device"></div>
</div>
<div class="section">
<div class="header">
<div class="title">Statistics</div>
</div>
<div class="body">
<div class="row"
v-for="(value, name) in status.stats"
:key="name">
<div class="param-name" v-text="name"></div>
<div class="param-value" v-text="value"></div>
</div>
</div>
</div>
</div>
</div>
</Modal>
<Modal title="Add nodes to group" ref="addNodesToGroupModal">
<div class="group-add">
<div class="params">
<div class="section">
<div class="header">
<div class="title">Select nodes to add</div>
</div>
<div class="body" v-if="selected.groupId != null">
<div class="row clickable" @click="addToGroup(node.node_id, selected.groupId)" :key="node.node_id"
v-for="node in Object.values(nodes || {}).filter((n) => groups[selected.groupId].associations.indexOf(n.node_id) < 0)">
<div class="param-name" v-text="node.name"></div>
</div>
</div>
</div>
</div>
</div>
</Modal>
<div class="view-options">
<div class="view-selector col-s-8 col-m-9 col-l-10">
<label>
<select @change="selected.view = $event.target.value">
<option v-for="(id, view) in views" :key="id"
v-text="(view[0].toUpperCase() + view.slice(1)).replace('_', ' ')"
:selected="view === selected.view" :value="view" />
</select>
</label>
</div>
<div class="buttons col-s-4 col-m-3 col-l-2">
<Dropdown title="Network commands" icon-class="fa fa-cog">
<DropdownItem text="Network Info" :disabled="commandRunning" @click="networkInfoModalOpen" />
<DropdownItem text="Start Network" :disabled="commandRunning" @click="startNetwork" />
<DropdownItem text="Stop Network" :disabled="commandRunning" @click="stopNetwork" />
<DropdownItem text="Add Scene" :disabled="commandRunning" @click="addScene" v-if="selected.view === 'scenes'" />
<DropdownItem text="Add Node" :disabled="commandRunning" @click="addNode" v-if="selected.view === 'nodes'" />
<DropdownItem text="Remove Node" :disabled="commandRunning" @click="removeNode"
v-if="selected.view === 'nodes'" />
<DropdownItem text="Switch All On" :disabled="commandRunning" @click="switchAll(true)" />
<DropdownItem text="Switch All Off" :disabled="commandRunning" @click="switchAll(false)" />
<DropdownItem text="Cancel Command" :disabled="commandRunning" @click="cancelCommand" />
<DropdownItem text="Kill Command" :disabled="commandRunning" @click="killCommand" />
<DropdownItem text="Receive Configuration" :disabled="commandRunning" @click="receiveConfiguration" />
<DropdownItem text="Create New Primary" :disabled="commandRunning" @click="createNewPrimary" />
<DropdownItem text="Transfer Primary Role" :disabled="commandRunning" @click="transferPrimaryRole" />
<DropdownItem text="Heal Network" :disabled="commandRunning" @click="healNetwork" />
<DropdownItem text="Soft Reset" :disabled="commandRunning" @click="softReset" />
<DropdownItem text="Hard Reset" :disabled="commandRunning" @click="hardReset" />
</Dropdown>
<button class="btn btn-default" title="Refresh Network" @click="refresh">
<i class="fa fa-sync-alt" />
</button>
</div>
</div>
<div class="view-container">
<div class="view nodes" v-if="selected.view === 'nodes'">
<Loading v-if="loading.nodes" />
<div class="no-items" v-else-if="!Object.keys(nodes || {}).length">
<div class="empty">No nodes available on the network</div>
</div>
<Node v-for="(node, nodeId) in nodes" :key="nodeId" :node="node" :selected="selected.nodeId === nodeId"
@select="onNodeClick(nodeId)" />
</div>
<div class="view groups" v-else-if="selected.view === 'groups'">
<Loading v-if="loading.groups" />
<div class="no-items" v-else-if="!Object.keys(groups || {}).length">
<div class="empty">No groups available on the network</div>
</div>
<Group v-for="(group, groupId) in groups" :key="groupId" :group="group" :selected="selected.groupId === groupId"
:nodes="groupId in groups ? groups[groupId].associations.map((node) => nodes[node]).
reduce((nodes, node) => {nodes[node.node_id] = node; return nodes}, {}) : {}"
@select="selected.groupId = groupId === selected.groupId ? undefined : groupId"
@open-add-nodes-to-group="$refs.addNodesToGroupModal.show()" />
</div>
<div class="view scenes" v-else-if="selected.view === 'scenes'">
<Loading v-if="loading.scenes" />
<div class="no-items" v-else-if="!Object.keys(scenes || {}).length">
<div class="empty">No scenes configured on the network</div>
</div>
<div class="item scene" :class="{selected: selected.sceneId === sceneId}"
v-for="(scene, sceneId) in scenes" :key="sceneId">
<div class="row name header vertical-center" :class="{selected: selected.sceneId === sceneId}" v-text="scene.label"
@click="selected.sceneId = sceneId === selected.sceneId ? undefined : sceneId" />
<div class="params" v-if="selected.sceneId === sceneId">
<div class="row">
<div class="param-name">Activate</div>
<div class="param-value">
<ToggleSwitch :value="false" @input="activateScene(sceneId)" />
</div>
</div>
<div class="section actions">
<div class="header">
<div class="title">Actions</div>
</div>
<div class="body">
<div class="row" @click="removeScene(sceneId)">
<div class="param-name">Remove Scene</div>
<div class="param-value">
<i class="fa fa-trash"></i>
</div>
</div>
<div class="row" @click="renameScene(sceneId)">
<div class="param-name">Rename Scene</div>
<div class="param-value">
<i class="fa fa-edit"></i>
</div>
</div>
</div>
</div>
<div class="section values" v-if="scene.values?.length">
<div class="value-container" v-for="(value, valueId) in valuesMap" :key="valueId">
<div class="value-display"
v-if="value.valueId && value.valueId in scenes.values[sceneId]" :scenes="scenes">
<Value :value="value" :node="node" :sceneId="sceneId" @add-to-scene="addValueToScene"
@remove-from-scene="removeValueFromScene" @refresh="refreshNodes" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="view values" v-else>
<Loading v-if="loading.nodes" />
<div class="no-items" v-else-if="!Object.keys(nodes || {}).length">
<div class="empty">No nodes found on the network</div>
</div>
<div class="node-container" v-for="(node, nodeId) in nodes" :key="nodeId">
<div class="item node"
:class="{selected: selected.nodeId === nodeId}"
v-if="selected.view === 'values' || Object.values(node.values).filter((value) => value.id_on_network in values[selected.view]).length > 0">
<div class="row name header vertical-center" :class="{selected: selected.nodeId === nodeId}" v-text="node.name"
@click="onNodeClick(nodeId)"></div>
<div class="params" v-if="selected.nodeId === nodeId">
<div class="value-container" v-for="(value, valueId) in node.values" :key="valueId">
<div class="value-display"
v-if="value.id_on_network && (selected.view === 'values' || value.id_on_network in values[selected.view])">
<Value :value="value" :node="node" :scenes="scenes" @add-to-scene="addValueToScene"
@remove-from-scene="removeValueFromScene" @refresh="refreshNodes" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import Group from "@/components/panels/Zwave/Group"; import Zwave from "@/components/panels/Zwave/Zwave";
import Node from "@/components/panels/Zwave/Node";
import Modal from "@/components/Modal";
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import Loading from "@/components/Loading";
import ToggleSwitch from "@/components/elements/ToggleSwitch";
import Value from "@/components/panels/Zwave/Value";
import Utils from "@/Utils";
export default { export default {
name: "Zwave", components: {Zwave},
components: {Value, ToggleSwitch, Loading, DropdownItem, Dropdown, Modal, Group, Node},
mixins: [Utils],
data() {
return {
status: {},
views: {},
nodes: {},
groups: {},
scenes: {},
commandRunning: false,
values: {
switches: {},
dimmers: {},
sensors: {},
battery_levels: {},
power_levels: {},
bulbs: {},
doorlocks: {},
usercodes: {},
thermostats: {},
protections: {},
},
selected: {
view: 'nodes',
nodeId: undefined,
groupId: undefined,
sceneId: undefined,
valueId: undefined,
},
loading: {
status: false,
nodes: false,
groups: false,
scenes: false,
},
}
},
computed: {
valuesMap() {
const values = {}
for (const node of Object.values(this.nodes)) {
for (const value of Object.values(node.values)) {
values[value.id_on_network] = value
}
}
return values
},
},
methods: {
async refreshNodes() {
this.loading.nodes = true
try {
this.nodes = await this.request('zwave.get_nodes')
} finally {
this.loading.nodes = false
}
if (Object.keys(this.nodes || {}).length)
this.views.values = true
},
async refreshGroups() {
this.loading.groups = true
try {
this.groups = Object.values(await this.request('zwave.get_groups'))
.filter((group) => group.index)
.reduce((groups, group) => {
groups[group.index] = group
return groups
}, {})
} finally {
this.loading.groups = false
}
if (Object.keys(this.groups || {}).length)
this.views.groups = true
},
async refreshScenes() {
this.loading.scenes = true
try {
this.scenes = Object.values(await this.request('zwave.get_scenes'))
.filter((scene) => scene.scene_id)
.reduce((scenes, scene) => {
scenes[scene.scene_id] = scene
return scenes
}, {})
} finally {
this.loading.scenes = false
}
if (Object.keys(this.scenes || {}).length)
this.views.values = true
},
async refreshValues(type) {
this.loading.values = true
try {
this.values[type] = Object.values(await this.request('zwave.get_' + type))
.filter((item) => item.id_on_network)
.reduce((values, value) => {
values[value.id_on_network] = true
return values
}, {})
} finally {
this.loading.values = false
}
if (Object.keys(this.values[type]).length)
this.views[type] = true
},
async refreshStatus() {
this.loading.status = true
try {
this.status = await this.request('zwave.status')
} finally {
this.loading.status = false
}
},
refresh() {
this.views = {
nodes: true,
scenes: true,
}
this.refreshNodes()
this.refreshGroups()
this.refreshScenes()
this.refreshValues('switches')
this.refreshValues('dimmers')
this.refreshValues('sensors')
this.refreshValues('bulbs')
this.refreshValues('doorlocks')
this.refreshValues('usercodes')
this.refreshValues('thermostats')
this.refreshValues('protections')
this.refreshValues('battery_levels')
this.refreshValues('power_levels')
this.refreshValues('node_config')
this.refreshStatus()
},
async addScene() {
let name = prompt('Scene name')
if (name?.length)
name = name.trim()
if (!name?.length)
return
this.commandRunning = true
try {
await this.request('zwave.create_scene', {label: name})
await this.refreshScenes()
} finally {
this.commandRunning = false
}
},
async removeScene(sceneId) {
if (!confirm('Are you sure that you want to delete this scene?'))
return
this.commandRunning = true
try {
await this.request('zwave.remove_scene', {scene_id: sceneId})
await this.refreshScenes()
} finally {
this.commandRunning = false
}
},
onNodeUpdate(event) {
this.nodes[event.node.node_id] = event.node
if (event.value)
this.nodes[event.node.node_id].values[event.value.id_on_network] = event.value
},
onNodeClick(nodeId) {
this.selected.nodeId = nodeId === this.selected.nodeId ? undefined : nodeId
},
networkInfoModalOpen() {
this.refreshStatus()
this.$refs.networkInfoModal.show()
},
onCommandEvent(event) {
if (event.error && event.error.length) {
this.notify({
text: event.state_description + ': ' + event.error_description,
error: true,
})
}
},
async addNode() {
this.commandRunning = true
try {
await this.request('zwave.add_node')
} finally {
this.commandRunning = false
}
await this.refreshNodes()
},
async addToGroup(nodeId, groupId) {
this.commandRunning = true
try {
await this.request('zwave.add_node_to_group', {
node_id: nodeId,
group_index: groupId,
})
} finally {
this.commandRunning = false
}
await this.refreshGroups()
},
async removeNode() {
this.commandRunning = true
try {
await this.request('zwave.remove_node')
} finally {
this.commandRunning = false
}
await this.refreshNodes()
},
async removeValueFromScene(event) {
if (!confirm('Are you sure that you want to remove this value from the scene?'))
return
this.commandRunning = true
try {
await this.request('zwave.scene_remove_value', {
id_on_network: event.valueId,
scene_id: event.sceneId,
})
} finally {
this.commandRunning = false
}
await this.refreshScenes()
},
async renameScene(sceneId) {
const scene = this.scenes[sceneId]
let name = prompt('New name', scene.label)
if (name)
name = name.trim()
if (!name?.length || name === scene.label)
return
this.commandRunning = true
try {
await this.request('zwave.set_scene_label', {
new_label: name,
scene_id: sceneId,
})
} finally {
this.commandRunning = false
}
await this.refreshScenes()
},
async startNetwork() {
this.commandRunning = true
try {
await this.request('zwave.start_network')
} finally {
this.commandRunning = false
}
},
async stopNetwork() {
this.commandRunning = true
try {
await this.request('zwave.stop_network')
} finally {
this.commandRunning = false
}
},
async switchAll(state) {
this.commandRunning = true
try {
await this.request('zwave.switch_all', {state: state})
this.refresh()
} finally {
this.commandRunning = false
}
},
async cancelCommand() {
this.commandRunning = true
try {
await this.request('zwave.cancel_command')
} finally {
this.commandRunning = false
}
},
async killCommand() {
this.commandRunning = true
try {
await this.request('zwave.kill_command')
} finally {
this.commandRunning = false
}
},
async setControllerName() {
let name = prompt('Controller name')
if (name?.length)
name = name.trim()
if (!name?.length)
return
this.commandRunning = true
try {
await this.request('zwave.set_controller_name', {name: name})
} finally {
this.commandRunning = false
}
this.refresh()
},
async receiveConfiguration() {
this.commandRunning = true
try {
await this.request('zwave.receive_configuration')
} finally {
this.commandRunning = false
}
this.refresh()
},
async createNewPrimary() {
this.commandRunning = true
try {
await this.request('zwave.create_new_primary')
} finally {
this.commandRunning = false
}
this.refresh()
},
async transferPrimaryRole() {
this.commandRunning = true
try {
await this.request('zwave.transfer_primary_role')
} finally {
this.commandRunning = false
}
this.refresh()
},
async healNetwork() {
this.commandRunning = true
try {
await this.request('zwave.heal')
} finally {
this.commandRunning = false
}
this.refresh()
},
async softReset() {
if (!confirm("Are you sure that you want to do a device soft reset? This won't lose network information"))
return
await this.request('zwave.soft_reset')
},
async hardReset() {
if (!confirm("Are you sure that you want to do a device soft reset? All network information will be LOST!"))
return
await this.request('zwave.hard_reset')
},
async activateScene(sceneId) {
this.commandRunning = true
try {
await this.request('zwave.activate_scene', {scene_id: sceneId})
} finally {
this.commandRunning = false
}
},
async addValueToScene(event) {
if (!this.selected.valueId)
return
this.commandRunning = true
try {
await this.request('zwave.scene_add_value', {
id_on_network: event.valueId,
scene_id: event.sceneId,
})
} finally {
this.commandRunning = false
}
this.refresh()
},
},
mounted() {
this.refresh()
this.subscribe(this.refreshGroups, 'on-zwave-node-group-event',
'platypush.message.event.zwave.ZwaveNodeGroupEvent')
this.subscribe(this.refreshScenes, 'on-zwave-node-scene-event',
'platypush.message.event.zwave.ZwaveNodeSceneEvent')
this.subscribe(this.refreshNodes, 'on-zwave-node-removed-event',
'platypush.message.event.zwave.ZwaveNodeRemovedEvent')
this.subscribe(this.onCommandEvent, 'on-zwave-command-event',
'platypush.message.event.zwave.ZwaveCommandEvent')
this.subscribe(this.refreshStatus, 'on-zwave-network-event',
'platypush.message.event.zwave.ZwaveNetworkReadyEvent',
'platypush.message.event.zwave.ZwaveNetworkStoppedEvent',
'platypush.message.event.zwave.ZwaveNetworkErrorEvent',
'platypush.message.event.zwave.ZwaveNetworkResetEvent')
this.subscribe(this.onNodeUpdate, 'on-zwave-node-update-event',
'platypush.message.event.zwave.ZwaveNodeEvent',
'platypush.message.event.zwave.ZwaveNodeAddedEvent',
'platypush.message.event.zwave.ZwaveNodeRenamedEvent',
'platypush.message.event.zwave.ZwaveNodeReadyEvent',
'platypush.message.event.zwave.ZwaveValueAddedEvent',
'platypush.message.event.zwave.ZwaveValueChangedEvent',
'platypush.message.event.zwave.ZwaveValueRemovedEvent',
'platypush.message.event.zwave.ZwaveValueRefreshedEvent')
},
unmounted() {
[
'on-zwave-node-group-event', 'on-zwave-node-scene-event', 'on-zwave-node-removed-event', 'on-zwave-command-event',
'on-zwave-network-event', 'on-zwave-node-update-event'
].forEach((eventType) => this.unsubscribe(eventType))
},
} }
</script> </script>
<style lang="scss">
@import "common";
.zwave-container {
width: 100%;
height: 100%;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
.view-options {
display: flex;
width: 100%;
height: $header-height;
justify-content: space-between;
align-items: center;
padding: 0;
background: $header-bg;
border-bottom: $default-border-2;
box-shadow: $border-shadow-bottom;
.view-selector {
display: inline-flex;
padding-left: .5em;
label {
width: 100%;
}
}
select {
width: 100%;
}
.buttons {
display: inline-flex;
justify-content: right;
margin: 0 !important;
button {
border: none;
background: none;
}
}
}
.group-add {
margin: -2em;
min-width: 20em;
padding-bottom: 1em;
}
.network-info {
margin: -1em;
}
}
</style>

View file

@ -36,9 +36,37 @@
</div> </div>
</div> </div>
<div class="row" v-if="node.location && node.location.length"> <div class="row">
<div class="param-name">Location</div> <div class="param-name">Location</div>
<div class="param-value" v-text="node.location" /> <div class="param-value">
<div class="edit-cell" :class="{hidden: !editMode.location}">
<form ref="locationForm" @submit.prevent="editLocation">
<label>
<input type="text" name="location" :value="node.location" :disabled="commandRunning">
</label>
<span class="buttons">
<button type="button" class="btn btn-default" @click="editMode.location = false">
<i class="fas fa-times" />
</button>
<button type="submit" class="btn btn-default" :disabled="commandRunning">
<i class="fa fa-check" />
</button>
</span>
</form>
</div>
<div :class="{hidden: editMode.location}">
<span v-text="node.location?.length ? node.location : ''" />
<span class="buttons">
<button type="button" class="btn btn-default" @click="onEditMode('location')"
:disabled="commandRunning">
<i class="fa fa-edit"></i>
</button>
</span>
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
@ -113,7 +141,7 @@
<div class="param-value" v-text="Object.values(node.groups).map((g) => g.label || '').join(', ')" /> <div class="param-value" v-text="Object.values(node.groups).map((g) => g.label || '').join(', ')" />
</div> </div>
<div class="row"> <div class="row" v-if="node.home_id">
<div class="param-name">Home ID</div> <div class="param-name">Home ID</div>
<div class="param-value" v-text="node.home_id.toString(16)" /> <div class="param-value" v-text="node.home_id.toString(16)" />
</div> </div>
@ -123,17 +151,22 @@
<div class="param-value" v-text="node.is_awake" /> <div class="param-value" v-text="node.is_awake" />
</div> </div>
<div class="row"> <div class="row" v-if="node.is_locked != null">
<div class="param-name">Is Locked</div> <div class="param-name">Is Locked</div>
<div class="param-value" v-text="node.is_locked" /> <div class="param-value" v-text="node.is_locked" />
</div> </div>
<div class="row" v-if="node.last_update"> <div class="row" v-if="node.last_update">
<div class="param-name">Last Update</div> <div class="param-name">Last Update</div>
<div class="param-value" v-text="node.last_update" /> <div class="param-value" v-text="formatDateTime(node.last_update)" />
</div> </div>
<div class="row" v-if="node.last_update"> <div class="row" v-if="node.baud_rate">
<div class="param-name">Baud Rate</div>
<div class="param-value" v-text="node.baud_rate" />
</div>
<div class="row" v-if="node.max_baud_rate">
<div class="param-name">Max Baud Rate</div> <div class="param-name">Max Baud Rate</div>
<div class="param-value" v-text="node.max_baud_rate" /> <div class="param-value" v-text="node.max_baud_rate" />
</div> </div>
@ -192,12 +225,12 @@
</template> </template>
<script> <script>
import Utils from "@/Utils"; import mixin from "@/components/panels/Zwave/mixin";
export default { export default {
name: "Node", name: "Node",
emits: ['select'], emits: ['select'],
mixins: [Utils], mixins: [mixin],
props: { props: {
node: { node: {
@ -216,6 +249,7 @@ export default {
commandRunning: false, commandRunning: false,
editMode: { editMode: {
name: false, name: false,
location: false,
}, },
} }
}, },
@ -235,7 +269,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.remove_node', { await this.zrequest('remove_node', {
node_id: this.node.node_id, node_id: this.node.node_id,
}) })
} finally { } finally {
@ -257,7 +291,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.replace_node', { await this.zrequest('replace_node', {
node_id: this.node.node_id, node_id: this.node.node_id,
}) })
} finally { } finally {
@ -276,7 +310,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.replication_send', { await this.zrequest('replication_send', {
node_id: this.node.node_id, node_id: this.node.node_id,
}) })
} finally { } finally {
@ -295,7 +329,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.request_network_update', { await this.zrequest('request_network_update', {
node_id: this.node.node_id, node_id: this.node.node_id,
}) })
} finally { } finally {
@ -314,7 +348,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.request_node_neighbour_update', { await this.zrequest('request_node_neighbour_update', {
node_id: this.node.node_id, node_id: this.node.node_id,
}) })
} finally { } finally {
@ -338,7 +372,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.set_node_name', { await this.zrequest('set_node_name', {
node_id: this.node.node_id, node_id: this.node.node_id,
new_name: name, new_name: name,
}) })
@ -349,6 +383,22 @@ export default {
this.editMode.name = false this.editMode.name = false
}, },
async editLocation(event) {
const location = event.target.querySelector('input[name=location]').value
this.commandRunning = true
try {
await this.zrequest('set_node_location', {
node_id: this.node.node_id,
location: location,
})
} finally {
this.commandRunning = false
}
this.editMode.location = false
},
async heal() { async heal() {
if (this.commandRunning) { if (this.commandRunning) {
console.log('A command is already running') console.log('A command is already running')
@ -357,7 +407,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.node_heal', { await this.zrequest('node_heal', {
node_id: this.node.node_id, node_id: this.node.node_id,
}) })
} finally { } finally {

View file

@ -25,10 +25,10 @@
<label> <label>
<select @change="onValueChange"> <select @change="onValueChange">
<option v-for="(data, index) in value.data_items" <option v-for="(data, index) in value.data_items"
v-text="data" v-text="typeof data === 'object' ? data.text : data"
:key="index" :key="index"
:selected="value.data === data" :selected="typeof data === 'object' ? value.data === data.value : value.data === data"
:value="index"> :value="typeof data === 'object' ? data.value : index">
</option> </option>
</select> </select>
</label> </label>
@ -99,19 +99,19 @@
<div class="param-value" v-text="value.value_id"></div> <div class="param-value" v-text="value.value_id"></div>
</div> </div>
<div class="row"> <div class="row" v-if="value.value_id !== value.id_on_network">
<div class="param-name">ID on Network</div> <div class="param-name">ID on Network</div>
<div class="param-value" v-text="value.id_on_network"></div> <div class="param-value" v-text="value.id_on_network"></div>
</div> </div>
<div class="row"> <div class="row">
<div class="param-name">Command Class</div> <div class="param-name">Command Class</div>
<div class="param-value" v-text="value.command_class"></div> <div class="param-value" v-text="value.command_class_name || value.command_class"></div>
</div> </div>
<div class="row" v-if="value.last_update"> <div class="row" v-if="value.last_update">
<div class="param-name">Last Update</div> <div class="param-name">Last Update</div>
<div class="param-value" v-text="value.last_update"></div> <div class="param-value" v-text="formatDateTime(value.last_update)"></div>
</div> </div>
</div> </div>
</div> </div>
@ -121,13 +121,13 @@
import Dropdown from "@/components/elements/Dropdown"; import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem"; import DropdownItem from "@/components/elements/DropdownItem";
import ToggleSwitch from "@/components/elements/ToggleSwitch"; import ToggleSwitch from "@/components/elements/ToggleSwitch";
import Utils from "@/Utils";
import Slider from "@/components/elements/Slider"; import Slider from "@/components/elements/Slider";
import mixin from "@/components/panels/Zwave/mixin";
export default { export default {
name: "Value", name: "Value",
components: {Slider, Dropdown, DropdownItem, ToggleSwitch}, components: {Slider, Dropdown, DropdownItem, ToggleSwitch},
mixins: [Utils], mixins: [mixin],
emits: ['remove-from-scene', 'add-to-scene', 'refresh'], emits: ['remove-from-scene', 'add-to-scene', 'refresh'],
props: { props: {
@ -177,7 +177,7 @@ export default {
this.commandRunning = true this.commandRunning = true
try { try {
await this.request('zwave.set_value_label', { await this.zrequest('set_value_label', {
id_on_network: value.id_on_network, id_on_network: value.id_on_network,
new_label: name, new_label: name,
}) })
@ -217,9 +217,12 @@ export default {
break break
} }
if (typeof data === 'object')
data = data.value
this.commandRunning = true this.commandRunning = true
try { try {
this.request('zwave.set_value', { await this.zrequest('set_value', {
id_on_network: value.id_on_network, id_on_network: value.id_on_network,
data: data, data: data,
}) })

View file

@ -0,0 +1,743 @@
<template>
<div class="zwave-container">
<Modal title="Network info" ref="networkInfoModal">
<div class="network-info">
<Loading v-if="loading.status" />
<div class="params" v-else>
<div class="row">
<div class="param-name">State</div>
<div class="param-value" v-text="status.state"></div>
</div>
<div class="row">
<div class="param-name">Device</div>
<div class="param-value" v-text="status.device"></div>
</div>
<div class="section">
<div class="header">
<div class="title">Statistics</div>
</div>
<div class="body">
<div class="row"
v-for="(value, name) in status.stats"
:key="name">
<div class="param-name" v-text="name"></div>
<div class="param-value" v-text="value"></div>
</div>
</div>
</div>
</div>
</div>
</Modal>
<Modal title="Add nodes to group" ref="addNodesToGroupModal">
<div class="group-add">
<div class="params">
<div class="section">
<div class="header">
<div class="title">Select nodes to add</div>
</div>
<div class="body" v-if="selected.groupId != null">
<div class="row clickable" @click="addToGroup(node.node_id, selected.groupId)" :key="node.node_id"
v-for="node in Object.values(nodes || {}).filter(
(n) => groups[selected.groupId].associations.indexOf(n.node_id) < 0)">
<div class="param-name" v-text="node.name"></div>
</div>
</div>
</div>
</div>
</div>
</Modal>
<div class="view-options">
<div class="view-selector col-s-6 col-m-8 col-l-9">
<label>
<select @change="selected.view = $event.target.value">
<option v-for="(id, view) in views" :key="id"
v-text="(view[0].toUpperCase() + view.slice(1)).replace('_', ' ')"
:selected="view === selected.view" :value="view" />
</select>
</label>
</div>
<div class="buttons col-s-6 col-m-4 col-l-3">
<button class="btn btn-default" title="Create Scene" @click="addScene" v-if="selected.view === 'scenes'">
<i class="fa fa-plus" />
</button>
<Dropdown title="Network commands" icon-class="fa fa-cog">
<DropdownItem text="Network Info" :disabled="commandRunning" @click="networkInfoModalOpen" />
<DropdownItem text="Start Network" :disabled="commandRunning" @click="startNetwork" />
<DropdownItem text="Stop Network" :disabled="commandRunning" @click="stopNetwork" />
<DropdownItem text="Add Node" :disabled="commandRunning" @click="addNode" v-if="selected.view === 'nodes'" />
<DropdownItem text="Remove Node" :disabled="commandRunning" @click="removeNode"
v-if="selected.view === 'nodes'" />
<DropdownItem text="Switch All On" :disabled="commandRunning" @click="switchAll(true)" />
<DropdownItem text="Switch All Off" :disabled="commandRunning" @click="switchAll(false)" />
<DropdownItem text="Cancel Command" :disabled="commandRunning" @click="cancelCommand" />
<DropdownItem text="Kill Command" :disabled="commandRunning" @click="killCommand" />
<DropdownItem text="Receive Configuration" :disabled="commandRunning" @click="receiveConfiguration" />
<DropdownItem text="Create New Primary" :disabled="commandRunning" @click="createNewPrimary" />
<DropdownItem text="Transfer Primary Role" :disabled="commandRunning" @click="transferPrimaryRole" />
<DropdownItem text="Heal Network" :disabled="commandRunning" @click="healNetwork" />
<DropdownItem text="Soft Reset" :disabled="commandRunning" @click="softReset" />
<DropdownItem text="Hard Reset" :disabled="commandRunning" @click="hardReset" />
</Dropdown>
<button class="btn btn-default" title="Refresh Network" @click="refresh">
<i class="fa fa-sync-alt" />
</button>
</div>
</div>
<div class="view-container">
<div class="view nodes" v-if="selected.view === 'nodes'">
<Loading v-if="loading.nodes" />
<div class="no-items" v-else-if="!Object.keys(nodes || {}).length">
<div class="empty">No nodes available on the network</div>
</div>
<Node v-for="(node, nodeId) in nodes" :key="nodeId" :node="node" :selected="selected.nodeId === nodeId"
:plugin-name="pluginName" @select="onNodeClick(nodeId)" />
</div>
<div class="view groups" v-else-if="selected.view === 'groups'">
<Loading v-if="loading.groups" />
<div class="no-items" v-else-if="!Object.keys(groups || {}).length">
<div class="empty">No groups available on the network</div>
</div>
<Group v-for="(group, groupId) in groups" :key="groupId" :group="group" :selected="selected.groupId === groupId"
:nodes="groupId in groups ? groups[groupId].associations.map((node) => nodes[node]).
reduce((nodes, node) => {nodes[node.node_id] = node; return nodes}, {}) : {}"
:owner="group.node_id != null ? nodes[group.node_id] : null" :plugin-name="pluginName"
@select="selected.groupId = groupId === selected.groupId ? undefined : groupId"
@open-add-nodes-to-group="$refs.addNodesToGroupModal.show()" />
</div>
<div class="view scenes" v-else-if="selected.view === 'scenes'">
<Loading v-if="loading.scenes" />
<div class="no-items" v-else-if="!Object.keys(scenes || {}).length">
<div class="empty">No scenes configured on the network</div>
</div>
<div class="item scene" :class="{selected: selected.sceneId === sceneId}"
v-for="(scene, sceneId) in scenes" :key="sceneId">
<div class="row name header vertical-center" :class="{selected: selected.sceneId === sceneId}" v-text="scene.label"
@click="selected.sceneId = sceneId === selected.sceneId ? undefined : sceneId" />
<div class="params" v-if="selected.sceneId === sceneId">
<div class="row">
<div class="param-name">Scene ID</div>
<div class="param-value" v-text="sceneId" />
</div>
<div class="row">
<div class="param-name">Activate</div>
<div class="param-value">
<ToggleSwitch :value="false" @input="activateScene(sceneId)" />
</div>
</div>
<div class="section values" v-if="Object.values(scene?.values)?.length">
<div class="header">
<div class="title">Values</div>
</div>
<div class="body">
<div class="row" v-for="value in Object.values(scene.values)" :key="value.id_on_network">
<div class="param-name">
{{ nodes[value.node_id].name }} &#8680; {{ valuesMap[value.id_on_network].label }}
</div>
<div class="param-value">
<span v-text="value.data" />
<span class="buttons">
<button class="btn btn-default" title="Remove value"
@click="removeValueFromScene({sceneId: sceneId, valueId: value.id_on_network})">
<i class="fa fa-trash" />
</button>
</span>
</div>
</div>
</div>
</div>
<div class="section actions">
<div class="header">
<div class="title">Actions</div>
</div>
<div class="body">
<div class="row" @click="removeScene(sceneId)">
<div class="param-name">Remove Scene</div>
<div class="param-value">
<i class="fa fa-trash" />
</div>
</div>
<div class="row" @click="renameScene(sceneId)">
<div class="param-name">Rename Scene</div>
<div class="param-value">
<i class="fa fa-edit" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="view values" v-else>
<Loading v-if="loading.nodes" />
<div class="no-items" v-else-if="!Object.keys(nodes || {}).length">
<div class="empty">No nodes found on the network</div>
</div>
<div class="node-container" v-for="(node, nodeId) in nodes" :key="nodeId">
<div class="item node"
:class="{selected: selected.nodeId === nodeId}"
v-if="selected.view === 'values' || Object.values(node.values).filter((value) => value.id_on_network in values[selected.view]).length > 0">
<div class="row name header vertical-center" :class="{selected: selected.nodeId === nodeId}" v-text="node.name"
@click="onNodeClick(nodeId)"></div>
<div class="params" v-if="selected.nodeId === nodeId">
<div class="value-container" v-for="(value, valueId) in node.values" :key="valueId">
<div class="value-display"
v-if="value.id_on_network && (selected.view === 'values' || value.id_on_network in values[selected.view])">
<Value :value="value" :node="node" :scenes="scenes" @add-to-scene="addValueToScene"
@remove-from-scene="removeValueFromScene" @refresh="refreshNodes" :plugin-name="pluginName" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Group from "@/components/panels/Zwave/Group";
import Node from "@/components/panels/Zwave/Node";
import Modal from "@/components/Modal";
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import Loading from "@/components/Loading";
import ToggleSwitch from "@/components/elements/ToggleSwitch";
import Value from "@/components/panels/Zwave/Value";
import mixin from "@/components/panels/Zwave/mixin";
export default {
name: "Zwave",
components: {Value, ToggleSwitch, Loading, DropdownItem, Dropdown, Modal, Group, Node},
mixins: [mixin],
data() {
return {
status: {},
views: {},
nodes: {},
groups: {},
scenes: {},
commandRunning: false,
values: {
switches: {},
dimmers: {},
sensors: {},
battery_levels: {},
power_levels: {},
bulbs: {},
doorlocks: {},
usercodes: {},
thermostats: {},
protections: {},
},
selected: {
view: 'nodes',
nodeId: undefined,
groupId: undefined,
sceneId: undefined,
valueId: undefined,
},
loading: {
status: false,
nodes: false,
groups: false,
scenes: false,
},
}
},
computed: {
valuesMap() {
const values = {}
for (const node of Object.values(this.nodes)) {
for (const value of Object.values(node.values)) {
values[value.id_on_network] = value
}
}
return values
},
},
methods: {
async refreshNodes() {
this.loading.nodes = true
try {
this.nodes = await this.zrequest('get_nodes')
} finally {
this.loading.nodes = false
}
if (Object.keys(this.nodes || {}).length)
this.views.values = true
},
async refreshGroups() {
this.loading.groups = true
try {
this.groups = Object.values(await this.zrequest('get_groups'))
.filter((group) => group.index)
.reduce((groups, group) => {
const id = group.group_id || group.index
groups[id] = group
return groups
}, {})
} finally {
this.loading.groups = false
}
if (Object.keys(this.groups || {}).length)
this.views.groups = true
},
async refreshScenes() {
this.loading.scenes = true
try {
this.scenes = Object.values(await this.zrequest('get_scenes'))
.filter((scene) => scene.scene_id)
.reduce((scenes, scene) => {
scenes[scene.scene_id] = scene
return scenes
}, {})
} finally {
this.loading.scenes = false
}
if (Object.keys(this.scenes || {}).length)
this.views.values = true
},
async refreshValues(type) {
this.loading.values = true
try {
this.values[type] = Object.values(await this.zrequest('get_' + type))
.filter((item) => item.id_on_network)
.reduce((values, value) => {
values[value.id_on_network] = true
return values
}, {})
} finally {
this.loading.values = false
}
if (Object.keys(this.values[type]).length)
this.views[type] = true
},
async refreshStatus() {
this.loading.status = true
try {
this.status = await this.zrequest('status')
} finally {
this.loading.status = false
}
},
refresh() {
this.views = {
nodes: true,
scenes: true,
}
this.refreshNodes()
this.refreshGroups()
this.refreshScenes()
this.refreshValues('switches')
this.refreshValues('dimmers')
this.refreshValues('sensors')
this.refreshValues('bulbs')
this.refreshValues('doorlocks')
this.refreshValues('usercodes')
this.refreshValues('thermostats')
this.refreshValues('protections')
this.refreshValues('battery_levels')
this.refreshValues('power_levels')
this.refreshValues('node_config')
this.refreshStatus()
},
async addScene() {
let name = prompt('Scene name')
if (name?.length)
name = name.trim()
if (!name?.length)
return
this.commandRunning = true
try {
await this.zrequest('create_scene', {label: name})
await this.refreshScenes()
} finally {
this.commandRunning = false
}
},
async removeScene(sceneId) {
if (!confirm('Are you sure that you want to delete this scene?'))
return
this.commandRunning = true
try {
await this.zrequest('remove_scene', {scene_id: sceneId})
await this.refreshScenes()
} finally {
this.commandRunning = false
}
},
onNodeUpdate(event) {
this.nodes[event.node.node_id] = event.node
if (event.value)
this.nodes[event.node.node_id].values[event.value.id_on_network] = event.value
},
onNodeClick(nodeId) {
this.selected.nodeId = nodeId === this.selected.nodeId ? undefined : nodeId
},
networkInfoModalOpen() {
this.refreshStatus()
this.$refs.networkInfoModal.show()
},
onCommandEvent(event) {
if (event.error && event.error.length) {
this.notify({
text: event.state_description + ': ' + event.error_description,
error: true,
})
}
},
async addNode() {
this.commandRunning = true
try {
await this.zrequest('add_node')
} finally {
this.commandRunning = false
}
await this.refreshNodes()
},
async addToGroup(nodeId, groupId) {
this.commandRunning = true
try {
await this.zrequest('add_node_to_group', {
node_id: nodeId,
group_index: groupId,
})
} finally {
this.commandRunning = false
}
await this.refreshGroups()
},
async removeNode() {
this.commandRunning = true
try {
await this.zrequest('remove_node')
} finally {
this.commandRunning = false
}
await this.refreshNodes()
},
async removeValueFromScene(event) {
if (!confirm('Are you sure that you want to remove this value from the scene?'))
return
this.commandRunning = true
try {
await this.zrequest('scene_remove_value', {
id_on_network: event.valueId,
scene_id: event.sceneId,
})
} finally {
this.commandRunning = false
}
await this.refreshScenes()
},
async renameScene(sceneId) {
const scene = this.scenes[sceneId]
let name = prompt('New name', scene.label)
if (name)
name = name.trim()
if (!name?.length || name === scene.label)
return
this.commandRunning = true
try {
await this.zrequest('set_scene_label', {
new_label: name,
scene_id: sceneId,
})
} finally {
this.commandRunning = false
}
await this.refreshScenes()
},
async startNetwork() {
this.commandRunning = true
try {
await this.zrequest('start_network')
} finally {
this.commandRunning = false
}
},
async stopNetwork() {
this.commandRunning = true
try {
await this.zrequest('stop_network')
} finally {
this.commandRunning = false
}
},
async switchAll(state) {
this.commandRunning = true
try {
await this.zrequest('switch_all', {state: state})
this.refresh()
} finally {
this.commandRunning = false
}
},
async cancelCommand() {
this.commandRunning = true
try {
await this.zrequest('cancel_command')
} finally {
this.commandRunning = false
}
},
async killCommand() {
this.commandRunning = true
try {
await this.zrequest('kill_command')
} finally {
this.commandRunning = false
}
},
async receiveConfiguration() {
this.commandRunning = true
try {
await this.zrequest('receive_configuration')
} finally {
this.commandRunning = false
}
this.refresh()
},
async createNewPrimary() {
this.commandRunning = true
try {
await this.zrequest('create_new_primary')
} finally {
this.commandRunning = false
}
this.refresh()
},
async transferPrimaryRole() {
this.commandRunning = true
try {
await this.zrequest('transfer_primary_role')
} finally {
this.commandRunning = false
}
this.refresh()
},
async healNetwork() {
this.commandRunning = true
try {
await this.zrequest('heal')
} finally {
this.commandRunning = false
}
this.refresh()
},
async softReset() {
if (!confirm("Are you sure that you want to do a device soft reset? This won't lose network information"))
return
await this.zrequest('soft_reset')
},
async hardReset() {
if (!confirm("Are you sure that you want to do a device soft reset? All network information will be LOST!"))
return
await this.zrequest('hard_reset')
},
async activateScene(sceneId) {
this.commandRunning = true
try {
await this.zrequest('activate_scene', {scene_id: sceneId})
} finally {
this.commandRunning = false
}
},
async addValueToScene(event) {
this.commandRunning = true
try {
await this.zrequest('scene_add_value', {
id_on_network: event.valueId,
scene_id: event.sceneId,
data: this.valuesMap[event.valueId].data,
})
} finally {
this.commandRunning = false
}
this.refresh()
},
},
mounted() {
this.refresh()
this.subscribe(this.refreshGroups, 'on-zwave-node-group-event',
'platypush.message.event.zwave.ZwaveNodeGroupEvent')
this.subscribe(this.refreshScenes, 'on-zwave-node-scene-event',
'platypush.message.event.zwave.ZwaveNodeSceneEvent')
this.subscribe(this.refreshNodes, 'on-zwave-node-removed-event',
'platypush.message.event.zwave.ZwaveNodeRemovedEvent')
this.subscribe(this.onCommandEvent, 'on-zwave-command-event',
'platypush.message.event.zwave.ZwaveCommandEvent')
this.subscribe(this.refreshStatus, 'on-zwave-network-event',
'platypush.message.event.zwave.ZwaveNetworkReadyEvent',
'platypush.message.event.zwave.ZwaveNetworkStoppedEvent',
'platypush.message.event.zwave.ZwaveNetworkErrorEvent',
'platypush.message.event.zwave.ZwaveNetworkResetEvent')
this.subscribe(this.onNodeUpdate, 'on-zwave-node-update-event',
'platypush.message.event.zwave.ZwaveNodeEvent',
'platypush.message.event.zwave.ZwaveNodeAddedEvent',
'platypush.message.event.zwave.ZwaveNodeRenamedEvent',
'platypush.message.event.zwave.ZwaveNodeReadyEvent',
'platypush.message.event.zwave.ZwaveValueAddedEvent',
'platypush.message.event.zwave.ZwaveValueChangedEvent',
'platypush.message.event.zwave.ZwaveValueRemovedEvent',
'platypush.message.event.zwave.ZwaveValueRefreshedEvent')
},
unmounted() {
[
'on-zwave-node-group-event', 'on-zwave-node-scene-event', 'on-zwave-node-removed-event', 'on-zwave-command-event',
'on-zwave-network-event', 'on-zwave-node-update-event'
].forEach((eventType) => this.unsubscribe(eventType))
},
}
</script>
<style lang="scss">
@import "common";
.zwave-container {
width: 100%;
height: 100%;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
.view-options {
display: flex;
width: 100%;
height: $header-height;
justify-content: space-between;
align-items: center;
padding: 0;
background: $header-bg;
border-bottom: $default-border-2;
box-shadow: $border-shadow-bottom;
.view-selector {
display: inline-flex;
padding-left: .5em;
label {
width: 100%;
}
}
select {
width: 100%;
}
.buttons {
display: inline-flex;
justify-content: right;
margin: 0 !important;
button {
border: none;
background: none;
}
}
}
.group-add {
margin: -2em;
min-width: 20em;
padding-bottom: 1em;
}
.network-info {
margin: -1em;
}
}
</style>

View file

@ -110,7 +110,7 @@
.unit { .unit {
font-size: .8em; font-size: .8em;
margin-left: 1em; margin-left: .5em;
display: inline; display: inline;
} }
@ -218,7 +218,7 @@
.unit { .unit {
font-size: .8em; font-size: .8em;
margin-left: 1em; margin-left: .5em;
display: inline; display: inline;
} }

View file

@ -0,0 +1,17 @@
import Utils from "@/Utils";
export default {
mixins: [Utils],
props: {
pluginName: {
type: String,
required: true,
},
},
methods: {
async zrequest(method, args) {
return await this.request(`${this.pluginName}.${method}`, args)
},
}
}

View file

@ -0,0 +1,11 @@
<template>
<Zwave plugin-name="zwave.mqtt" />
</template>
<script>
import Zwave from "@/components/panels/Zwave/Zwave";
export default {
components: {Zwave},
}
</script>

View file

@ -0,0 +1,175 @@
import json
from queue import Queue, Empty
from typing import Optional, Type
from platypush.backend.mqtt import MqttBackend
from platypush.context import get_plugin
from platypush.message.event.zwave import ZwaveEvent, ZwaveNodeAddedEvent, ZwaveValueChangedEvent, \
ZwaveNodeRemovedEvent, ZwaveNodeRenamedEvent, ZwaveNodeReadyEvent, ZwaveNodeEvent, ZwaveNodeAsleepEvent, \
ZwaveNodeAwakeEvent
class ZwaveMqttBackend(MqttBackend):
"""
Listen for events on a `zwavejs2mqtt <https://github.com/zwave-js/zwavejs2mqtt>`_ service.
Triggers:
* :class:`platypush.message.event.zwave.ZwaveNodeEvent` when a node attribute changes.
* :class:`platypush.message.event.zwave.ZwaveNodeAddedEvent` when a node is added to the network.
* :class:`platypush.message.event.zwave.ZwaveNodeRemovedEvent` when a node is removed from the network.
* :class:`platypush.message.event.zwave.ZwaveNodeRenamedEvent` when a node is renamed.
* :class:`platypush.message.event.zwave.ZwaveNodeReadyEvent` when a node is ready.
* :class:`platypush.message.event.zwave.ZwaveValueChangedEvent` when the value of a node on the network
changes.
* :class:`platypush.message.event.zwave.ZwaveNodeAsleepEvent` when a node goes into sleep mode.
* :class:`platypush.message.event.zwave.ZwaveNodeAwakeEvent` when a node goes back into awake mode.
Requires:
* **paho-mqtt** (``pip install paho-mqtt``)
* A `zwavejs2mqtt instance <https://github.com/zwave-js/zwavejs2mqtt>`_.
* The :class:`platypush.plugins.zwave.mqtt.ZwaveMqttPlugin` plugin configured.
"""
def __init__(self, client_id: Optional[str] = None, *args, **kwargs):
"""
:param client_id: MQTT client ID (default: ``<device_id>-zwavejs-mqtt``, to prevent clashes with the
:class:`platypush.backend.mqtt.MqttBackend` ``client_id``.
"""
from platypush.plugins.zwave.mqtt import ZwaveMqttPlugin
self.plugin: ZwaveMqttPlugin = get_plugin('zwave.mqtt')
assert self.plugin, 'The zwave.mqtt plugin is not configured'
self._nodes = {}
self._groups = {}
self._last_state = None
self._events_queue = Queue()
self.events_topic = self.plugin.events_topic
self.server_info = {
'host': self.plugin.host,
'port': self.plugin.port or self._default_mqtt_port,
'tls_cafile': self.plugin.tls_cafile,
'tls_certfile': self.plugin.tls_certfile,
'tls_ciphers': self.plugin.tls_ciphers,
'tls_keyfile': self.plugin.tls_keyfile,
'tls_version': self.plugin.tls_version,
'username': self.plugin.username,
'password': self.plugin.password,
}
listeners = [{
**self.server_info,
'topics': [
self.plugin.events_topic + '/node/' + topic
for topic in ['node_ready', 'node_sleep', 'node_value_updated', 'node_metadata_updated', 'node_wakeup']
],
}]
super().__init__(*args, subscribe_default_topic=False, listeners=listeners, client_id=client_id, **kwargs)
if not client_id:
self.client_id += '-zwavejs-mqtt'
def _dispatch_event(self, event_type: Type[ZwaveEvent], node: Optional[dict] = None, value: Optional[dict] = None,
**kwargs):
if value and 'id' not in value:
value_id = f"{value['commandClass']}-{value.get('endpoint', 0)}-{value['property']}"
if 'propertyKey' in value:
value_id += '-' + value['propertyKey']
if value_id not in node.get('values', {}):
self.logger.warning(f'value_id {value_id} not found on node {node["id"]}')
return
value = node['values'][value_id]
if value:
kwargs['value'] = self.plugin.value_to_dict(value)
if node:
kwargs['node'] = self.plugin.node_to_dict(node)
node_id = kwargs['node']['node_id']
if event_type == ZwaveNodeEvent:
if node_id not in self._nodes:
event_type = ZwaveNodeAddedEvent
elif kwargs['node']['name'] != self._nodes[node_id]['name']:
event_type = ZwaveNodeRenamedEvent
if event_type == ZwaveNodeRemovedEvent:
self._nodes.pop(node_id, None)
else:
self._nodes[node_id] = kwargs['node']
evt = event_type(**kwargs)
self._events_queue.put(evt)
# zwavejs2mqtt currently treats some values (e.g. binary switches) in an inconsistent way,
# using two values - a read-only value called currentValue that gets updated on the
# node_value_updated topic, and a writable value called targetValue that doesn't get updated
# (see https://github.com/zwave-js/zwavejs2mqtt/blob/4a6a5c5f1274763fd3aced4cae2c72ea060716b5/docs/guide/migrating.md).
# To properly manage updates on writable values, propagate an event for both.
if event_type == ZwaveValueChangedEvent and kwargs.get('value', {}).get('property_id') == 'currentValue':
value = kwargs['value'].copy()
target_value_id = f'{kwargs["node"]["node_id"]}-{value["command_class"]}-{value.get("endpoint", 0)}' \
f'-targetValue'
kwargs['value'] = kwargs['node'].get('values', {}).get(target_value_id)
if kwargs['value']:
kwargs['value']['data'] = value['data']
kwargs['node']['values'][target_value_id] = kwargs['value']
evt = event_type(**kwargs)
self._events_queue.put(evt)
def on_mqtt_message(self):
def handler(_, __, msg):
if not msg.topic.startswith(self.events_topic):
return
topic = msg.topic[len(self.events_topic) + 1:].split('/').pop()
data = msg.payload.decode()
if not data:
return
try:
data = json.loads(data)['data']
except (ValueError, TypeError):
pass
try:
if topic == 'node_value_updated':
self._dispatch_event(ZwaveValueChangedEvent, node=data[0], value=data[1])
elif topic == 'node_metadata_updated':
self._dispatch_event(ZwaveNodeEvent, node=data[0])
elif topic == 'node_sleep':
self._dispatch_event(ZwaveNodeAsleepEvent, node=data[0])
elif topic == 'node_wakeup':
self._dispatch_event(ZwaveNodeAwakeEvent, node=data[0])
elif topic == 'node_ready':
self._dispatch_event(ZwaveNodeReadyEvent, node=data[0])
elif topic == 'node_removed':
self._dispatch_event(ZwaveNodeRemovedEvent, node=data[0])
except Exception as e:
self.logger.exception(e)
return handler
def run(self):
super().run()
self.logger.debug('Refreshing Z-Wave nodes')
# noinspection PyUnresolvedReferences
self._nodes = self.plugin.get_nodes().output
while not self.should_stop():
try:
evt = self._events_queue.get(block=True, timeout=1)
except Empty:
continue
self.bus.post(evt)
# vim:sw=4:ts=4:et:

View file

@ -1,9 +0,0 @@
from platypush.message.event.http import HttpEvent
class NewReservationEvent(HttpEvent):
def __init__(self, request, response, *args, **kwargs):
super().__init__(request=request, response=response, *args, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -91,6 +91,20 @@ class ZwaveNodeReadyEvent(ZwaveNodeEvent):
pass pass
class ZwaveNodeAsleepEvent(ZwaveNodeEvent):
"""
Triggered when a node goes in sleep mode.
"""
pass
class ZwaveNodeAwakeEvent(ZwaveNodeEvent):
"""
Triggered when a node goes back into awake mode.
"""
pass
class ZwaveNodeGroupEvent(ZwaveNodeEvent): class ZwaveNodeGroupEvent(ZwaveNodeEvent):
""" """
Triggered when a node is associated/de-associated to a group. Triggered when a node is associated/de-associated to a group.

View file

@ -988,7 +988,7 @@ class ZwavePlugin(SwitchPlugin):
@action @action
def off(self, device: str, *args, **kwargs): def off(self, device: str, *args, **kwargs):
""" """
Turn on a switch on a device. Turn off a switch on a device.
:param device: ``id_on_network`` of the value to be switched off. :param device: ``id_on_network`` of the value to be switched off.
""" """

View file

@ -0,0 +1,98 @@
command_classes = {
0x00: 'no_operation',
0x20: 'basic',
0x21: 'controller_replication',
0x22: 'application_status',
0x23: 'zip_services',
0x24: 'zip_server',
0x25: 'switch_binary',
0x26: 'switch_multilevel',
0x27: 'switch_all',
0x28: 'switch_toggle_binary',
0x29: 'switch_toggle_multilevel',
0x2a: 'chimney_fan',
0x2b: 'scene_activation',
0x2c: 'scene_actuator_conf',
0x2d: 'scene_controller_conf',
0x2e: 'zip_client',
0x2f: 'zip_adv_services',
0x30: 'sensor_binary',
0x31: 'sensor_multilevel',
0x32: 'meter',
0x33: 'color',
0x34: 'zip_adv_client',
0x35: 'meter_pulse',
0x3c: 'meter_tbl_config',
0x3d: 'meter_tbl_monitor',
0x3e: 'meter_tbl_pulse',
0x38: 'thermostat_heating',
0x40: 'thermostat_mode',
0x42: 'thermostat_operating_state',
0x43: 'thermostat_setpoint',
0x44: 'thermostat_fan_mode',
0x45: 'thermostat_fan_state',
0x46: 'climate_control_schedule',
0x47: 'thermostat_setback',
0x4c: 'door_lock_logging',
0x4e: 'schedule_entry_lock',
0x50: 'basic_window_covering',
0x51: 'mtp_window_covering',
0x56: 'crc16_encap',
0x5a: 'device_reset_locally',
0x5b: 'central_scene',
0x5e: 'zwave_plus_info',
0x5d: 'antitheft',
0x60: 'multi_instance',
0x62: 'door_lock',
0x63: 'user_code',
0x66: 'barrier_operator',
0x70: 'configuration',
0x71: 'notification',
0x72: 'manufacturer_specific',
0x73: 'powerlevel',
0x75: 'protection',
0x76: 'lock',
0x77: 'node_naming',
0x79: 'sound_switch',
0x7a: 'firmware_update_md',
0x7b: 'grouping_name',
0x7c: 'remote_association_activate',
0x7d: 'remote_association',
0x80: 'battery',
0x81: 'clock',
0x82: 'hail',
0x84: 'wake_up',
0x85: 'association',
0x86: 'version',
0x87: 'indicator',
0x88: 'proprietary',
0x89: 'language',
0x8a: 'time',
0x8b: 'time_parameters',
0x8c: 'geographic_location',
0x8d: 'composite',
0x8e: 'multi_instance_association',
0x8f: 'multi_cmd',
0x90: 'energy_production',
0x91: 'manufacturer_proprietary',
0x92: 'screen_md',
0x93: 'screen_attributes',
0x94: 'simple_av_control',
0x95: 'av_content_directory_md',
0x96: 'av_renderer_status',
0x97: 'av_content_search_md',
0x98: 'security',
0x99: 'av_tagging_md',
0x9a: 'ip_configuration',
0x9b: 'association_command_configuration',
0x9c: 'sensor_alarm',
0x9d: 'silence_alarm',
0x9e: 'sensor_configuration',
0xef: 'mark',
0xf0: 'non_interoperable'
}
command_class_by_name = {
name: code
for code, name in command_classes.items()
}

File diff suppressed because it is too large Load diff