Added zwave.mqtt plugin and backend [closes #186]
This commit is contained in:
parent
75e1f35523
commit
c006c4b368
44 changed files with 2798 additions and 790 deletions
|
@ -5,6 +5,10 @@ Given the high speed of development in the first phase, changes are being report
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added zwavejs2mqtt integration (see [#186](https://git.platypush.tech/platypush/platypush/-/issues/186).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Major LINT fixes.
|
||||
|
|
|
@ -80,3 +80,4 @@ Backends
|
|||
platypush/backend/wiimote.rst
|
||||
platypush/backend/zigbee.mqtt.rst
|
||||
platypush/backend/zwave.rst
|
||||
platypush/backend/zwave.mqtt.rst
|
||||
|
|
5
docs/source/platypush/backend/zwave.mqtt.rst
Normal file
5
docs/source/platypush/backend/zwave.mqtt.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.zwave.mqtt``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.zwave.mqtt
|
||||
:members:
|
5
docs/source/platypush/plugins/zwave.mqtt.rst
Normal file
5
docs/source/platypush/plugins/zwave.mqtt.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.zwave.mqtt``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.plugins.zwave.mqtt
|
||||
:members:
|
|
@ -145,3 +145,4 @@ Plugins
|
|||
platypush/plugins/zeroconf.rst
|
||||
platypush/plugins/zigbee.mqtt.rst
|
||||
platypush/plugins/zwave.rst
|
||||
platypush/plugins/zwave.mqtt.rst
|
||||
|
|
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-41adab28.4173158a.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-41adab28.4173158a.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
platypush/backend/http/webapp/dist/static/css/chunk-a13d55c8.2d455d68.css
vendored
Normal file
7
platypush/backend/http/webapp/dist/static/css/chunk-a13d55c8.2d455d68.css
vendored
Normal file
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
2
platypush/backend/http/webapp/dist/static/js/app.e46423d1.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/app.e46423d1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/app.e46423d1.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/app.e46423d1.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/chunk-2d0ac54d.74f539e1.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/chunk-2d0ac54d.74f539e1.js
vendored
Normal 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
|
1
platypush/backend/http/webapp/dist/static/js/chunk-2d0ac54d.74f539e1.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/chunk-2d0ac54d.74f539e1.js.map
vendored
Normal 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":""}
|
2
platypush/backend/http/webapp/dist/static/js/chunk-2d0b21a7.4a00e73b.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/chunk-2d0b21a7.4a00e73b.js
vendored
Normal 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
|
1
platypush/backend/http/webapp/dist/static/js/chunk-2d0b21a7.4a00e73b.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/chunk-2d0b21a7.4a00e73b.js.map
vendored
Normal 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":""}
|
2
platypush/backend/http/webapp/dist/static/js/chunk-31bc5041.fa7aa55d.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/chunk-31bc5041.fa7aa55d.js
vendored
Normal 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
|
1
platypush/backend/http/webapp/dist/static/js/chunk-31bc5041.fa7aa55d.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/chunk-31bc5041.fa7aa55d.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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
2
platypush/backend/http/webapp/dist/static/js/chunk-41adab28.8ac6033c.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/chunk-41adab28.8ac6033c.js
vendored
Normal 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
|
1
platypush/backend/http/webapp/dist/static/js/chunk-41adab28.8ac6033c.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/chunk-41adab28.8ac6033c.js.map
vendored
Normal 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
2
platypush/backend/http/webapp/dist/static/js/chunk-a13d55c8.a46f6a41.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/chunk-a13d55c8.a46f6a41.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/chunk-a13d55c8.a46f6a41.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/chunk-a13d55c8.a46f6a41.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -68,6 +68,9 @@
|
|||
},
|
||||
"zwave": {
|
||||
"imgUrl": "/icons/z-wave.png"
|
||||
},
|
||||
"zwave.mqtt": {
|
||||
"imgUrl": "/icons/z-wave.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -4,6 +4,16 @@
|
|||
@click="$emit('select', group.index)" />
|
||||
|
||||
<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="header">
|
||||
<div class="title col-10">Nodes</div>
|
||||
|
@ -50,18 +60,21 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/Utils";
|
||||
import mixin from "@/components/panels/Zwave/mixin";
|
||||
|
||||
export default {
|
||||
name: "Group",
|
||||
emits: ['select', 'open-add-nodes-to-group'],
|
||||
mixins: [Utils],
|
||||
mixins: [mixin],
|
||||
|
||||
props: {
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
owner: {
|
||||
type: Object,
|
||||
},
|
||||
nodes: {
|
||||
type: Object,
|
||||
default: () => { return {} },
|
||||
|
@ -84,11 +97,17 @@ export default {
|
|||
return
|
||||
|
||||
this.commandRunning = true
|
||||
const args = {
|
||||
node_id: nodeId,
|
||||
}
|
||||
|
||||
if (this.group.group_id != null)
|
||||
args.group_id = this.group.group_id
|
||||
else
|
||||
args.group_index = this.group.index
|
||||
|
||||
try {
|
||||
await this.request('zwave.remove_node_from_group', {
|
||||
node_id: nodeId,
|
||||
group_index: this.group.index,
|
||||
})
|
||||
await this.zrequest('remove_node_from_group', args)
|
||||
} finally {
|
||||
this.commandRunning = false
|
||||
}
|
||||
|
@ -99,4 +118,17 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
@import "common";
|
||||
|
||||
.section.nodes {
|
||||
.header, .row {
|
||||
position: relative;
|
||||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,738 +1,11 @@
|
|||
<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-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>
|
||||
<Zwave plugin-name="zwave" />
|
||||
</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 Utils from "@/Utils";
|
||||
import Zwave from "@/components/panels/Zwave/Zwave";
|
||||
|
||||
export default {
|
||||
name: "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))
|
||||
},
|
||||
components: {Zwave},
|
||||
}
|
||||
</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>
|
||||
|
|
|
@ -36,9 +36,37 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="node.location && node.location.length">
|
||||
<div class="row">
|
||||
<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 class="row">
|
||||
|
@ -113,7 +141,7 @@
|
|||
<div class="param-value" v-text="Object.values(node.groups).map((g) => g.label || '').join(', ')" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" v-if="node.home_id">
|
||||
<div class="param-name">Home ID</div>
|
||||
<div class="param-value" v-text="node.home_id.toString(16)" />
|
||||
</div>
|
||||
|
@ -123,17 +151,22 @@
|
|||
<div class="param-value" v-text="node.is_awake" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" v-if="node.is_locked != null">
|
||||
<div class="param-name">Is Locked</div>
|
||||
<div class="param-value" v-text="node.is_locked" />
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="node.last_update">
|
||||
<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 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-value" v-text="node.max_baud_rate" />
|
||||
</div>
|
||||
|
@ -192,12 +225,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/Utils";
|
||||
import mixin from "@/components/panels/Zwave/mixin";
|
||||
|
||||
export default {
|
||||
name: "Node",
|
||||
emits: ['select'],
|
||||
mixins: [Utils],
|
||||
mixins: [mixin],
|
||||
|
||||
props: {
|
||||
node: {
|
||||
|
@ -216,6 +249,7 @@ export default {
|
|||
commandRunning: false,
|
||||
editMode: {
|
||||
name: false,
|
||||
location: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -235,7 +269,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.remove_node', {
|
||||
await this.zrequest('remove_node', {
|
||||
node_id: this.node.node_id,
|
||||
})
|
||||
} finally {
|
||||
|
@ -257,7 +291,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.replace_node', {
|
||||
await this.zrequest('replace_node', {
|
||||
node_id: this.node.node_id,
|
||||
})
|
||||
} finally {
|
||||
|
@ -276,7 +310,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.replication_send', {
|
||||
await this.zrequest('replication_send', {
|
||||
node_id: this.node.node_id,
|
||||
})
|
||||
} finally {
|
||||
|
@ -295,7 +329,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.request_network_update', {
|
||||
await this.zrequest('request_network_update', {
|
||||
node_id: this.node.node_id,
|
||||
})
|
||||
} finally {
|
||||
|
@ -314,7 +348,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.request_node_neighbour_update', {
|
||||
await this.zrequest('request_node_neighbour_update', {
|
||||
node_id: this.node.node_id,
|
||||
})
|
||||
} finally {
|
||||
|
@ -338,7 +372,7 @@ export default {
|
|||
this.commandRunning = true
|
||||
|
||||
try {
|
||||
await this.request('zwave.set_node_name', {
|
||||
await this.zrequest('set_node_name', {
|
||||
node_id: this.node.node_id,
|
||||
new_name: name,
|
||||
})
|
||||
|
@ -349,6 +383,22 @@ export default {
|
|||
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() {
|
||||
if (this.commandRunning) {
|
||||
console.log('A command is already running')
|
||||
|
@ -357,7 +407,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.node_heal', {
|
||||
await this.zrequest('node_heal', {
|
||||
node_id: this.node.node_id,
|
||||
})
|
||||
} finally {
|
||||
|
|
|
@ -25,10 +25,10 @@
|
|||
<label>
|
||||
<select @change="onValueChange">
|
||||
<option v-for="(data, index) in value.data_items"
|
||||
v-text="data"
|
||||
v-text="typeof data === 'object' ? data.text : data"
|
||||
:key="index"
|
||||
:selected="value.data === data"
|
||||
:value="index">
|
||||
:selected="typeof data === 'object' ? value.data === data.value : value.data === data"
|
||||
:value="typeof data === 'object' ? data.value : index">
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
@ -99,19 +99,19 @@
|
|||
<div class="param-value" v-text="value.value_id"></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-value" v-text="value.id_on_network"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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 class="row" v-if="value.last_update">
|
||||
<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>
|
||||
|
@ -121,13 +121,13 @@
|
|||
import Dropdown from "@/components/elements/Dropdown";
|
||||
import DropdownItem from "@/components/elements/DropdownItem";
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
import Utils from "@/Utils";
|
||||
import Slider from "@/components/elements/Slider";
|
||||
import mixin from "@/components/panels/Zwave/mixin";
|
||||
|
||||
export default {
|
||||
name: "Value",
|
||||
components: {Slider, Dropdown, DropdownItem, ToggleSwitch},
|
||||
mixins: [Utils],
|
||||
mixins: [mixin],
|
||||
emits: ['remove-from-scene', 'add-to-scene', 'refresh'],
|
||||
|
||||
props: {
|
||||
|
@ -177,7 +177,7 @@ export default {
|
|||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
await this.request('zwave.set_value_label', {
|
||||
await this.zrequest('set_value_label', {
|
||||
id_on_network: value.id_on_network,
|
||||
new_label: name,
|
||||
})
|
||||
|
@ -217,9 +217,12 @@ export default {
|
|||
break
|
||||
}
|
||||
|
||||
if (typeof data === 'object')
|
||||
data = data.value
|
||||
|
||||
this.commandRunning = true
|
||||
try {
|
||||
this.request('zwave.set_value', {
|
||||
await this.zrequest('set_value', {
|
||||
id_on_network: value.id_on_network,
|
||||
data: data,
|
||||
})
|
||||
|
|
|
@ -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 }} ⇨ {{ 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>
|
|
@ -110,7 +110,7 @@
|
|||
|
||||
.unit {
|
||||
font-size: .8em;
|
||||
margin-left: 1em;
|
||||
margin-left: .5em;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
@ -218,7 +218,7 @@
|
|||
|
||||
.unit {
|
||||
font-size: .8em;
|
||||
margin-left: 1em;
|
||||
margin-left: .5em;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<Zwave plugin-name="zwave.mqtt" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Zwave from "@/components/panels/Zwave/Zwave";
|
||||
|
||||
export default {
|
||||
components: {Zwave},
|
||||
}
|
||||
</script>
|
175
platypush/backend/zwave/mqtt.py
Normal file
175
platypush/backend/zwave/mqtt.py
Normal 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:
|
|
@ -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:
|
||||
|
|
@ -91,6 +91,20 @@ class ZwaveNodeReadyEvent(ZwaveNodeEvent):
|
|||
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):
|
||||
"""
|
||||
Triggered when a node is associated/de-associated to a group.
|
||||
|
|
|
@ -988,7 +988,7 @@ class ZwavePlugin(SwitchPlugin):
|
|||
@action
|
||||
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.
|
||||
"""
|
||||
|
|
98
platypush/plugins/zwave/_constants.py
Normal file
98
platypush/plugins/zwave/_constants.py
Normal 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()
|
||||
}
|
1547
platypush/plugins/zwave/mqtt.py
Normal file
1547
platypush/plugins/zwave/mqtt.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue