Migrated switches web panel and refactored switch plugins to expose a more consistent interface

This commit is contained in:
Fabio Manganiello 2021-02-19 02:54:12 +01:00
parent 51de11da25
commit 56f8d85feb
59 changed files with 1167 additions and 65 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

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-01396ebc"],{b4ff:function(e,t,c){},bedd:function(e,t,c){"use strict";c.r(t);c("b0c0"),c("b64b");var n=c("7a23"),a=Object(n["K"])("data-v-5c936ba2");Object(n["u"])("data-v-5c936ba2");var s={class:"switches wemo-switches"},o={key:1,class:"no-content"},i={key:0,class:"switch-info"},b={class:"row"},d=Object(n["h"])("div",{class:"name"},"Name",-1),l={class:"row"},O=Object(n["h"])("div",{class:"name"},"On",-1),j={class:"row"},v=Object(n["h"])("div",{class:"name"},"IP",-1);Object(n["s"])();var r=a((function(e,t,c,r,u,f){var h=Object(n["z"])("Loading"),w=Object(n["z"])("Switch"),g=Object(n["z"])("Modal");return Object(n["r"])(),Object(n["e"])("div",s,[e.loading?(Object(n["r"])(),Object(n["e"])(h,{key:0})):Object.keys(e.devices).length?Object(n["f"])("",!0):(Object(n["r"])(),Object(n["e"])("div",o,"No WeMo 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"])(w,{loading:e.loading,name:c,state:t.on,onToggle:function(t){return e.toggle(c)},key:c,"has-info":!0,onInfo:function(t){e.selectedDevice=c,e.$refs.switchInfoModal.show()}},null,8,["loading","name","state","onToggle","onInfo"])})),128)),Object(n["h"])(g,{title:"Device Info",ref:"switchInfoModal"},{default:a((function(){return[e.selectedDevice?(Object(n["r"])(),Object(n["e"])("div",i,[Object(n["h"])("div",b,[d,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].name)},null,8,["textContent"])]),Object(n["h"])("div",l,[O,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].on)},null,8,["textContent"])]),Object(n["h"])("div",j,[v,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].ip)},null,8,["textContent"])])])):Object(n["f"])("",!0)]})),_:1},512)])})),u=c("3a5e"),f=c("487b"),h=c("17dc"),w=c("714b"),g={name:"SwitchWemo",components:{Modal:w["a"],Switch:h["a"],Loading:u["a"]},mixins:[f["a"]]};c("c319");g.render=r,g.__scopeId="data-v-5c936ba2";t["default"]=g},c319:function(e,t,c){"use strict";c("b4ff")}}]);
//# sourceMappingURL=chunk-01396ebc.c5c193f1.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Switches/SwitchWemo/Index.vue","webpack:///./src/components/panels/Switches/SwitchWemo/Index.vue?77ee","webpack:///./src/components/panels/Switches/SwitchWemo/Index.vue?7731"],"names":["class","loading","Object","keys","devices","length","device","name","state","on","toggle","key","has-info","selectedDevice","$refs","switchInfoModal","show","title","ref","ip","components","Modal","Switch","Loading","mixins","render","__scopeId"],"mappings":"+PACOA,MAAM,0B,SAEJA,MAAM,c,SAOJA,MAAM,e,GACJA,MAAM,O,EACT,eAA4B,OAAvBA,MAAM,QAAO,QAAI,G,GAInBA,MAAM,O,EACT,eAA0B,OAArBA,MAAM,QAAO,MAAE,G,GAIjBA,MAAM,O,EACT,eAA0B,OAArBA,MAAM,QAAO,MAAE,G,6JArB5B,eA0BM,MA1BN,EA0BM,CAzBW,EAAAC,S,iBAAf,eAA0B,YACUC,OAAOC,KAAK,EAAAC,SAASC,O,wCAAzD,eAA8F,MAA9F,EAAiE,6B,mBAEjE,eAEsE,2BADrC,EAAAD,SAAO,SAAxBE,EAAQC,G,wBADxB,eAEsE,GAF7DN,QAAS,EAAAA,QAAUM,KAAMA,EAAOC,MAAOF,EAAOG,GAAK,SAAM,mBAAE,EAAAC,OAAOH,IAChCI,IAAKJ,EAAOK,YAAU,EACxD,OAAI,YAAE,EAAAC,eAAiBN,EAAM,EAAAO,MAAMC,gBAAgBC,S,gEAE5D,eAiBQ,GAjBDC,MAAM,cAAcC,IAAI,mB,YAC7B,iBAeM,CAfyB,EAAAL,gB,iBAA/B,eAeM,MAfN,EAeM,CAdJ,eAGM,MAHN,EAGM,CAFJ,EACA,eAA2D,OAAtDb,MAAM,Q,YAAQ,eAAqC,EAAtB,QAAC,EAAAa,gBAAgBN,O,0BAGrD,eAGM,MAHN,EAGM,CAFJ,EACA,eAAyD,OAApDP,MAAM,Q,YAAQ,eAAmC,EAApB,QAAC,EAAAa,gBAAgBJ,K,0BAGrD,eAGM,MAHN,EAGM,CAFJ,EACA,eAAyD,OAApDT,MAAM,Q,YAAQ,eAAmC,EAApB,QAAC,EAAAa,gBAAgBM,K,sHAa9C,GACbZ,KAAM,aACNa,WAAY,CAACC,QAAA,KAAOC,SAAA,KAAQC,UAAA,MAC5BC,OAAQ,CAAC,S,UClCX,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ,gB,kCCRf","file":"static/js/chunk-01396ebc.c5c193f1.js","sourcesContent":["<template>\n <div class=\"switches wemo-switches\">\n <Loading v-if=\"loading\" />\n <div class=\"no-content\" v-else-if=\"!Object.keys(devices).length\">No WeMo switches found.</div>\n\n <Switch :loading=\"loading\" :name=\"name\" :state=\"device.on\" @toggle=\"toggle(name)\"\n v-for=\"(device, name) in devices\" :key=\"name\" :has-info=\"true\"\n @info=\"selectedDevice = name; $refs.switchInfoModal.show()\" />\n\n <Modal title=\"Device Info\" ref=\"switchInfoModal\">\n <div class=\"switch-info\" v-if=\"selectedDevice\">\n <div class=\"row\">\n <div class=\"name\">Name</div>\n <div class=\"value\" v-text=\"devices[selectedDevice].name\" />\n </div>\n\n <div class=\"row\">\n <div class=\"name\">On</div>\n <div class=\"value\" v-text=\"devices[selectedDevice].on\" />\n </div>\n\n <div class=\"row\">\n <div class=\"name\">IP</div>\n <div class=\"value\" v-text=\"devices[selectedDevice].ip\" />\n </div>\n </div>\n </Modal>\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\";\nimport Modal from \"@/components/Modal\";\n\nexport default {\n name: \"SwitchWemo\",\n components: {Modal, 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=5c936ba2&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=5c936ba2&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-5c936ba2\"\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/cli-service/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/cli-service/node_modules/vue-loader-v16/dist/index.js??ref--0-1!./Index.vue?vue&type=style&index=0&id=5c936ba2&lang=scss&scoped=true\""],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-194b2204"],{"65d6":function(e,t,c){"use strict";c.r(t);c("b64b");var n=c("7a23"),o=Object(n["K"])("data-v-616a9486");Object(n["u"])("data-v-616a9486");var a={class:"switches zigbee-mqtt-switches"},i={key:1,class:"no-content"},s={key:0,class:"switch-info"};Object(n["s"])();var b=o((function(e,t,c,b,d,f){var r=Object(n["z"])("Loading"),l=Object(n["z"])("Switch"),j=Object(n["z"])("Modal");return Object(n["r"])(),Object(n["e"])("div",a,[e.loading?(Object(n["r"])(),Object(n["e"])(r,{key:0})):Object.keys(e.devices).length?Object(n["f"])("",!0):(Object(n["r"])(),Object(n["e"])("div",i,"No Zigbee 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"])(l,{loading:e.loading,name:c,state:t.on,onToggle:function(t){return e.toggle(c)},key:c,"has-info":!0,onInfo:function(t){e.selectedDevice=c,e.$refs.switchInfoModal.show()}},null,8,["loading","name","state","onToggle","onInfo"])})),128)),Object(n["h"])(j,{title:"Device Info",ref:"switchInfoModal"},{default:o((function(){return[e.selectedDevice?(Object(n["r"])(),Object(n["e"])("div",s)):Object(n["f"])("",!0)]})),_:1},512)])})),d=c("3a5e"),f=c("487b"),r=c("17dc"),l=c("714b"),j={name:"ZigbeeMqtt",components:{Modal:l["a"],Switch:r["a"],Loading:d["a"]},mixins:[f["a"]]};c("7eff");j.render=b,j.__scopeId="data-v-616a9486";t["default"]=j},"7eff":function(e,t,c){"use strict";c("8d51")},"8d51":function(e,t,c){}}]);
//# sourceMappingURL=chunk-194b2204.ce93c763.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Switches/ZigbeeMqtt/Index.vue","webpack:///./src/components/panels/Switches/ZigbeeMqtt/Index.vue?fd1a","webpack:///./src/components/panels/Switches/ZigbeeMqtt/Index.vue?91d8"],"names":["class","loading","Object","keys","devices","length","device","name","state","on","toggle","key","has-info","selectedDevice","$refs","switchInfoModal","show","title","ref","components","Modal","Switch","Loading","mixins","render","__scopeId"],"mappings":"gOACOA,MAAM,iC,SAEJA,MAAM,c,SAOJA,MAAM,e,6JATf,eA0BM,MA1BN,EA0BM,CAzBW,EAAAC,S,iBAAf,eAA0B,YACUC,OAAOC,KAAK,EAAAC,SAASC,O,wCAAzD,eAAgG,MAAhG,EAAiE,+B,mBAEjE,eAEsE,2BADrC,EAAAD,SAAO,SAAxBE,EAAQC,G,wBADxB,eAEsE,GAF7DN,QAAS,EAAAA,QAAUM,KAAMA,EAAOC,MAAOF,EAAOG,GAAK,SAAM,mBAAE,EAAAC,OAAOH,IAChCI,IAAKJ,EAAOK,YAAU,EACxD,OAAI,YAAE,EAAAC,eAAiBN,EAAM,EAAAO,MAAMC,gBAAgBC,S,gEAE5D,eAiBQ,GAjBDC,MAAM,cAAcC,IAAI,mB,YAC7B,iBAeM,CAfyB,EAAAL,gB,iBAA/B,eAeM,MAfN,I,yFA0BS,GACbN,KAAM,aACNY,WAAY,CAACC,QAAA,KAAOC,SAAA,KAAQC,UAAA,MAC5BC,OAAQ,CAAC,S,UClCX,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ,gB,oCCRf,W","file":"static/js/chunk-194b2204.ce93c763.js","sourcesContent":["<template>\n <div class=\"switches zigbee-mqtt-switches\">\n <Loading v-if=\"loading\" />\n <div class=\"no-content\" v-else-if=\"!Object.keys(devices).length\">No Zigbee switches found.</div>\n\n <Switch :loading=\"loading\" :name=\"name\" :state=\"device.on\" @toggle=\"toggle(name)\"\n v-for=\"(device, name) in devices\" :key=\"name\" :has-info=\"true\"\n @info=\"selectedDevice = name; $refs.switchInfoModal.show()\" />\n\n <Modal title=\"Device Info\" ref=\"switchInfoModal\">\n <div class=\"switch-info\" v-if=\"selectedDevice\">\n<!-- <div class=\"row\">-->\n<!-- <div class=\"name\">Name</div>-->\n<!-- <div class=\"value\" v-text=\"devices[selectedDevice].name\" />-->\n<!-- </div>-->\n\n<!-- <div class=\"row\">-->\n<!-- <div class=\"name\">On</div>-->\n<!-- <div class=\"value\" v-text=\"devices[selectedDevice].on\" />-->\n<!-- </div>-->\n\n<!-- <div class=\"row\">-->\n<!-- <div class=\"name\">Address</div>-->\n<!-- <div class=\"value\" v-text=\"devices[selectedDevice].address\" />-->\n<!-- </div>-->\n </div>\n </Modal>\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\";\nimport Modal from \"@/components/Modal\";\n\nexport default {\n name: \"ZigbeeMqtt\",\n components: {Modal, 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=616a9486&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=616a9486&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-616a9486\"\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/cli-service/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/cli-service/node_modules/vue-loader-v16/dist/index.js??ref--0-1!./Index.vue?vue&type=style&index=0&id=616a9486&lang=scss&scoped=true\""],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-22aa1dbc"],{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},"84aa":function(e,n,t){"use strict";t("cd7e")},c1da:function(e,n,t){var c={"./LightHue/Index":["0219","chunk-2f304dee","chunk-cf32428c","chunk-58df877b"],"./SwitchSwitchbot/Index":["5083","chunk-2f304dee","chunk-cf32428c","chunk-4748c324"],"./SwitchTplink/Index":["d11f","chunk-2f304dee","chunk-cf32428c","chunk-2ce5c25d"],"./SwitchWemo/Index":["bedd","chunk-2f304dee","chunk-cf32428c","chunk-01396ebc"],"./ZigbeeMqtt/Index":["65d6","chunk-2f304dee","chunk-cf32428c","chunk-194b2204"]};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},cd7e:function(e,n,t){}}]);
//# sourceMappingURL=chunk-22aa1dbc.c2ddea8b.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-2ce5c25d"],{3340:function(e,t,c){"use strict";c("981d")},"981d":function(e,t,c){},d11f:function(e,t,c){"use strict";c.r(t);c("b0c0"),c("b64b");var n=c("7a23"),i=Object(n["K"])("data-v-e259fb36");Object(n["u"])("data-v-e259fb36");var s={class:"switches tplink-switches"},l={key:1,class:"no-content"},d={key:0,class:"switch-info"},o={class:"row"},v=Object(n["h"])("div",{class:"name"},"Name",-1),a={class:"row"},b=Object(n["h"])("div",{class:"name"},"On",-1),O={class:"row"},j=Object(n["h"])("div",{class:"name"},"IP",-1),r={key:0,class:"row"},u=Object(n["h"])("div",{class:"name"},"MAC",-1),h={key:1,class:"row"},w=Object(n["h"])("div",{class:"name"},"Current Consumption",-1),f={key:2,class:"row"},C=Object(n["h"])("div",{class:"name"},"Device Type",-1),m={key:3,class:"row"},D=Object(n["h"])("div",{class:"name"},"Firmware ID",-1),_={key:4,class:"row"},x=Object(n["h"])("div",{class:"name"},"Hardware ID",-1),k={key:5,class:"row"},p=Object(n["h"])("div",{class:"name"},"Hardware Version",-1),g={key:6,class:"row"},y=Object(n["h"])("div",{class:"name"},"Software Version",-1);Object(n["s"])();var I=i((function(e,t,c,I,M,T){var S=Object(n["z"])("Loading"),z=Object(n["z"])("Switch"),L=Object(n["z"])("Modal");return Object(n["r"])(),Object(n["e"])("div",s,[e.loading?(Object(n["r"])(),Object(n["e"])(S,{key:0})):Object.keys(e.devices).length?Object(n["f"])("",!0):(Object(n["r"])(),Object(n["e"])("div",l,"No TP-Link 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"])(z,{loading:e.loading,name:c,state:t.on,onToggle:function(t){return e.toggle(c)},key:c,"has-info":!0,onInfo:function(t){e.selectedDevice=c,e.$refs.switchInfoModal.show()}},null,8,["loading","name","state","onToggle","onInfo"])})),128)),Object(n["h"])(L,{title:"Device Info",ref:"switchInfoModal"},{default:i((function(){var t,c,i,s,l,I;return[e.selectedDevice?(Object(n["r"])(),Object(n["e"])("div",d,[Object(n["h"])("div",o,[v,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].name)},null,8,["textContent"])]),Object(n["h"])("div",a,[b,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].on)},null,8,["textContent"])]),Object(n["h"])("div",O,[j,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].ip)},null,8,["textContent"])]),(null===(t=e.devices[e.selectedDevice].hw_info)||void 0===t?void 0:t.mac)?(Object(n["r"])(),Object(n["e"])("div",r,[u,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].hw_info.mac)},null,8,["textContent"])])):Object(n["f"])("",!0),null!=e.devices[e.selectedDevice].current_consumption?(Object(n["r"])(),Object(n["e"])("div",h,[w,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].current_consumption)},null,8,["textContent"])])):Object(n["f"])("",!0),(null===(c=e.devices[e.selectedDevice].hw_info)||void 0===c?void 0:c.dev_name)?(Object(n["r"])(),Object(n["e"])("div",f,[C,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].hw_info.dev_name)},null,8,["textContent"])])):Object(n["f"])("",!0),(null===(i=e.devices[e.selectedDevice].hw_info)||void 0===i?void 0:i.fwId)?(Object(n["r"])(),Object(n["e"])("div",m,[D,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].hw_info.fwId)},null,8,["textContent"])])):Object(n["f"])("",!0),(null===(s=e.devices[e.selectedDevice].hw_info)||void 0===s?void 0:s.hwId)?(Object(n["r"])(),Object(n["e"])("div",_,[x,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].hw_info.hwId)},null,8,["textContent"])])):Object(n["f"])("",!0),(null===(l=e.devices[e.selectedDevice].hw_info)||void 0===l?void 0:l.hw_ver)?(Object(n["r"])(),Object(n["e"])("div",k,[p,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].hw_info.hw_ver)},null,8,["textContent"])])):Object(n["f"])("",!0),(null===(I=e.devices[e.selectedDevice].hw_info)||void 0===I?void 0:I.sw_ver)?(Object(n["r"])(),Object(n["e"])("div",g,[y,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].hw_info.sw_ver)},null,8,["textContent"])])):Object(n["f"])("",!0)])):Object(n["f"])("",!0)]})),_:1},512)])})),M=c("3a5e"),T=c("487b"),S=c("17dc"),z=c("714b"),L={name:"SwitchTplink",components:{Modal:z["a"],Switch:S["a"],Loading:M["a"]},mixins:[T["a"]]};c("3340");L.render=I,L.__scopeId="data-v-e259fb36";t["default"]=L}}]);
//# sourceMappingURL=chunk-2ce5c25d.de32e4fb.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-4748c324"],{"021c":function(e,t,c){"use strict";c("edec")},5083:function(e,t,c){"use strict";c.r(t);c("b0c0"),c("b64b");var n=c("7a23"),s=Object(n["K"])("data-v-7b062fcf");Object(n["u"])("data-v-7b062fcf");var a={class:"switches switchbot-switches"},i={key:1,class:"no-content"},o={key:0,class:"switch-info"},d={class:"row"},b=Object(n["h"])("div",{class:"name"},"Name",-1),l={class:"row"},O=Object(n["h"])("div",{class:"name"},"On",-1),j={class:"row"},v=Object(n["h"])("div",{class:"name"},"Address",-1);Object(n["s"])();var r=s((function(e,t,c,r,f,u){var h=Object(n["z"])("Loading"),w=Object(n["z"])("Switch"),g=Object(n["z"])("Modal");return Object(n["r"])(),Object(n["e"])("div",a,[e.loading?(Object(n["r"])(),Object(n["e"])(h,{key:0})):Object.keys(e.devices).length?Object(n["f"])("",!0):(Object(n["r"])(),Object(n["e"])("div",i,"No SwitchBot 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"])(w,{loading:e.loading,name:c,state:t.on,onToggle:function(t){return e.toggle(c)},key:c,"has-info":!0,onInfo:function(t){e.selectedDevice=c,e.$refs.switchInfoModal.show()}},null,8,["loading","name","state","onToggle","onInfo"])})),128)),Object(n["h"])(g,{title:"Device Info",ref:"switchInfoModal"},{default:s((function(){return[e.selectedDevice?(Object(n["r"])(),Object(n["e"])("div",o,[Object(n["h"])("div",d,[b,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].name)},null,8,["textContent"])]),Object(n["h"])("div",l,[O,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].on)},null,8,["textContent"])]),Object(n["h"])("div",j,[v,Object(n["h"])("div",{class:"value",textContent:Object(n["C"])(e.devices[e.selectedDevice].address)},null,8,["textContent"])])])):Object(n["f"])("",!0)]})),_:1},512)])})),f=c("3a5e"),u=c("487b"),h=c("17dc"),w=c("714b"),g={name:"SwitchSwitchbot",components:{Modal:w["a"],Switch:h["a"],Loading:f["a"]},mixins:[u["a"]]};c("021c");g.render=r,g.__scopeId="data-v-7b062fcf";t["default"]=g},edec:function(e,t,c){}}]);
//# sourceMappingURL=chunk-4748c324.d98e70eb.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Switches/SwitchSwitchbot/Index.vue?5a42","webpack:///./src/components/panels/Switches/SwitchSwitchbot/Index.vue","webpack:///./src/components/panels/Switches/SwitchSwitchbot/Index.vue?4da4"],"names":["class","loading","Object","keys","devices","length","device","name","state","on","toggle","key","has-info","selectedDevice","$refs","switchInfoModal","show","title","ref","address","components","Modal","Switch","Loading","mixins","render","__scopeId"],"mappings":"kHAAA,W,0JCCOA,MAAM,+B,SAEJA,MAAM,c,SAOJA,MAAM,e,GACJA,MAAM,O,EACT,eAA4B,OAAvBA,MAAM,QAAO,QAAI,G,GAInBA,MAAM,O,EACT,eAA0B,OAArBA,MAAM,QAAO,MAAE,G,GAIjBA,MAAM,O,EACT,eAA+B,OAA1BA,MAAM,QAAO,WAAO,G,6JArBjC,eA0BM,MA1BN,EA0BM,CAzBW,EAAAC,S,iBAAf,eAA0B,YACUC,OAAOC,KAAK,EAAAC,SAASC,O,wCAAzD,eAAmG,MAAnG,EAAiE,kC,mBAEjE,eAEsE,2BADrC,EAAAD,SAAO,SAAxBE,EAAQC,G,wBADxB,eAEsE,GAF7DN,QAAS,EAAAA,QAAUM,KAAMA,EAAOC,MAAOF,EAAOG,GAAK,SAAM,mBAAE,EAAAC,OAAOH,IAChCI,IAAKJ,EAAOK,YAAU,EACxD,OAAI,YAAE,EAAAC,eAAiBN,EAAM,EAAAO,MAAMC,gBAAgBC,S,gEAE5D,eAiBQ,GAjBDC,MAAM,cAAcC,IAAI,mB,YAC7B,iBAeM,CAfyB,EAAAL,gB,iBAA/B,eAeM,MAfN,EAeM,CAdJ,eAGM,MAHN,EAGM,CAFJ,EACA,eAA2D,OAAtDb,MAAM,Q,YAAQ,eAAqC,EAAtB,QAAC,EAAAa,gBAAgBN,O,0BAGrD,eAGM,MAHN,EAGM,CAFJ,EACA,eAAyD,OAApDP,MAAM,Q,YAAQ,eAAmC,EAApB,QAAC,EAAAa,gBAAgBJ,K,0BAGrD,eAGM,MAHN,EAGM,CAFJ,EACA,eAA8D,OAAzDT,MAAM,Q,YAAQ,eAAwC,EAAzB,QAAC,EAAAa,gBAAgBM,U,sHAa9C,GACbZ,KAAM,kBACNa,WAAY,CAACC,QAAA,KAAOC,SAAA,KAAQC,UAAA,MAC5BC,OAAQ,CAAC,S,UClCX,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ,gB","file":"static/js/chunk-4748c324.d98e70eb.js","sourcesContent":["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/cli-service/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/cli-service/node_modules/vue-loader-v16/dist/index.js??ref--0-1!./Index.vue?vue&type=style&index=0&id=7b062fcf&lang=scss&scoped=true\"","<template>\n <div class=\"switches switchbot-switches\">\n <Loading v-if=\"loading\" />\n <div class=\"no-content\" v-else-if=\"!Object.keys(devices).length\">No SwitchBot switches found.</div>\n\n <Switch :loading=\"loading\" :name=\"name\" :state=\"device.on\" @toggle=\"toggle(name)\"\n v-for=\"(device, name) in devices\" :key=\"name\" :has-info=\"true\"\n @info=\"selectedDevice = name; $refs.switchInfoModal.show()\" />\n\n <Modal title=\"Device Info\" ref=\"switchInfoModal\">\n <div class=\"switch-info\" v-if=\"selectedDevice\">\n <div class=\"row\">\n <div class=\"name\">Name</div>\n <div class=\"value\" v-text=\"devices[selectedDevice].name\" />\n </div>\n\n <div class=\"row\">\n <div class=\"name\">On</div>\n <div class=\"value\" v-text=\"devices[selectedDevice].on\" />\n </div>\n\n <div class=\"row\">\n <div class=\"name\">Address</div>\n <div class=\"value\" v-text=\"devices[selectedDevice].address\" />\n </div>\n </div>\n </Modal>\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\";\nimport Modal from \"@/components/Modal\";\n\nexport default {\n name: \"SwitchSwitchbot\",\n components: {Modal, 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=7b062fcf&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=7b062fcf&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-7b062fcf\"\n\nexport default script"],"sourceRoot":""}

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-cf32428c"],{"17dc":function(e,t,n){"use strict";n("b0c0");var i=n("7a23"),o=Object(i["K"])("data-v-755a3c5f");Object(i["u"])("data-v-755a3c5f");var a={class:"name col-l-10 col-m-9 col-s-8"},c=Object(i["h"])("i",{class:"fa fa-info"},null,-1),r={class:"toggler col-l-2 col-m-3 col-s-4"};Object(i["s"])();var s=o((function(e,t,n,o,s,u){var l=Object(i["z"])("Loading"),f=Object(i["z"])("ToggleSwitch");return Object(i["r"])(),Object(i["e"])("div",{class:"switch",onClick:t[2]||(t[2]=Object(i["J"])((function(){return u.onToggle.apply(u,arguments)}),["stop"]))},[n.loading?(Object(i["r"])(),Object(i["e"])(l,{key:0})):Object(i["f"])("",!0),Object(i["h"])("div",a,[n.hasInfo?(Object(i["r"])(),Object(i["e"])("button",{key:0,onClick:t[1]||(t[1]=Object(i["J"])((function(){return u.onInfo.apply(u,arguments)}),["prevent"]))},[c])):Object(i["f"])("",!0),Object(i["h"])("span",{class:"name-content",textContent:Object(i["C"])(n.name)},null,8,["textContent"])]),Object(i["h"])("div",r,[Object(i["h"])(f,{disabled:n.loading,value:n.state,onInput:u.onToggle},null,8,["disabled","value","onInput"])])])})),u=n("0279"),l=n("3a5e"),f={name:"Switch",components:{Loading:l["a"],ToggleSwitch:u["a"]},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1}},methods:{onInfo:function(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle:function(e){return e.stopPropagation(),this.$emit("toggle"),!1}}};n("21ae");f.render=s,f.__scopeId="data-v-755a3c5f";t["a"]=f},"21ae":function(e,t,n){"use strict";n("24a4")},"24a4":function(e,t,n){},"487b":function(e,t,n){"use strict";n("13d5"),n("b0c0"),n("96cf");var i=n("1da1"),o=n("3e54"),a={name:"SwitchesMixin",mixins:[o["a"]],props:{pluginName:{type:String,required:!0},bus:{type:Object,required:!0},config:{type:Object,default:function(){return{}}},selected:{type:Boolean,default:!1}},data:function(){return{loading:!1,initialized:!1,selectedDevice:null,devices:{}}},methods:{onRefreshEvent:function(e){e===this.pluginName&&this.refresh()},toggle:function(e){var t=this;return Object(i["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.next=2,t.request("".concat(t.pluginName,".toggle"),{device:e});case 2:i=n.sent,t.devices[e].on=i.on;case 4:case"end":return n.stop()}}),n)})))()},refresh:function(){var e=this;return Object(i["a"])(regeneratorRuntime.mark((function t(){return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return e.loading=!0,t.prev=1,t.next=4,e.request("".concat(e.pluginName,".status"));case 4:e.devices=t.sent.reduce((function(e,t){var n,i=(null===(n=t.name)||void 0===n?void 0:n.length)?t.name:t.id;return e[i]=t,e}),{});case 5:return t.prev=5,e.loading=!1,t.finish(5);case 8:case"end":return t.stop()}}),t,null,[[1,,5,8]])})))()}},mounted:function(){var e=this;this.$watch((function(){return e.selected}),(function(t){t&&!e.initialized&&(e.refresh(),e.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted:function(){this.bus.off("refresh",this.onRefreshEvent)}};t["a"]=a}}]);
//# sourceMappingURL=chunk-cf32428c.b3f244c0.js.map

File diff suppressed because one or more lines are too long

View file

@ -7176,6 +7176,11 @@
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
}, },
"lato-font": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lato-font/-/lato-font-3.0.0.tgz",
"integrity": "sha1-kbg34jdLZo+3Mx1EyJTTei2fjhE="
},
"launch-editor": { "launch-editor": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz",

View file

@ -12,6 +12,7 @@
"axios": "^0.21.0", "axios": "^0.21.0",
"bulma": "^0.9.1", "bulma": "^0.9.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lato-font": "^3.0.0",
"mitt": "^2.1.0", "mitt": "^2.1.0",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.0",

View file

@ -45,6 +45,9 @@
"rtorrent": { "rtorrent": {
"class": "fa fa-magnet" "class": "fa fa-magnet"
}, },
"switches": {
"class": "fas fa-toggle-on"
},
"sound": { "sound": {
"class": "fa fa-microphone" "class": "fa fa-microphone"
}, },

View file

@ -111,9 +111,7 @@ nav {
} }
@media screen and (min-width: $tablet) { @media screen and (min-width: $tablet) {
width: 20%; width: calc(16em - 2vw);
min-width: 12.5em;
max-width: 25em;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background: $nav-bg; background: $nav-bg;
@ -122,6 +120,10 @@ nav {
margin-right: 2px; margin-right: 2px;
} }
@media screen and (min-width: $desktop) {
width: 16em;
}
li { li {
box-shadow: $nav-box-shadow-entry; box-shadow: $nav-box-shadow-entry;
cursor: pointer; cursor: pointer;

View file

@ -0,0 +1,214 @@
<template>
<div class="switches-container">
<Loading v-if="loading" />
<div class="switch-plugins">
<div class="no-content" v-if="!Object.keys(plugins).length">No switch plugins configured</div>
<div class="switch-plugin" v-for="pluginName in Object.keys(plugins)" :key="pluginName"
@click="selectedPlugin = selectedPlugin === pluginName ? null : pluginName">
<div class="header" :class="{selected: selectedPlugin === pluginName}">
<div class="name col-10" v-text="pluginName" />
<div class="refresh col-2" v-if="selectedPlugin === pluginName">
<button @click.stop="bus.emit('refresh', pluginName)" title="Refresh plugin" :disabled="loading">
<i class="fa fa-sync" />
</button>
</div>
</div>
<div class="body" :class="{hidden: selectedPlugin !== pluginName}">
<component :is="components[pluginName]" :config="plugins[pluginName]" :plugin-name="pluginName"
:selected="selectedPlugin === pluginName" :bus="bus" />
</div>
</div>
</div>
<div class="refresh-button">
<button @click="refresh" :disabled="loading" title="Refresh plugins">
<i class="fa fa-sync" />
</button>
</div>
</div>
</template>
<script>
import Loading from "@/components/Loading";
import Utils from "@/Utils";
import {defineAsyncComponent} from "vue";
import mitt from "mitt";
export default {
name: "Switches",
components: {Loading},
mixins: [Utils],
data() {
return {
loading: false,
plugins: {},
components: {},
selectedPlugin: null,
bus: mitt(),
}
},
methods: {
initPanels() {
this.components = {}
Object.keys(this.plugins).forEach(async (pluginName) => {
const componentName = pluginName.split('.').map((token) => token[0].toUpperCase() + token.slice(1)).join('')
let comp = null
try {
comp = await import(`@/components/panels/Switches/${componentName}/Index`)
} catch (e) {
return
}
const component = defineAsyncComponent(async () => { return comp })
this.$options.components[pluginName] = component
this.components[pluginName] = component
})
},
async refresh() {
this.loading = true
try {
this.plugins = await this.request('utils.get_switch_plugins')
this.initPanels()
} finally {
this.loading = false
}
},
},
mounted() {
this.refresh()
},
}
</script>
<style lang="scss" scoped>
@import "vars";
.switches-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
.switch-plugins {
background: $background-color;
display: flex;
flex-direction: column;
box-shadow: $border-shadow-bottom-right;
@media screen and (max-width: calc(#{$tablet - 1px})) {
width: 100%;
}
@media screen and (min-width: $tablet) {
width: 90%;
border-radius: 1em;
margin-top: 2em;
}
@media screen and (min-width: $desktop) {
width: 500pt;
margin-top: 3em;
}
}
.no-content {
padding: 1.5em;
}
.switch-plugin {
display: flex;
flex-direction: column;
.header {
display: flex;
align-items: center;
padding: 1em 1.5em 1em .5em;
text-transform: uppercase;
letter-spacing: .075em;
border-bottom: $default-border-2;
cursor: pointer;
&:hover {
background: $hover-bg;
}
&.selected {
background: $selected-bg;
box-shadow: $border-shadow-bottom-right;
border: none;
}
.refresh {
text-align: right;
}
button {
padding: 0;
margin: 0;
border: none;
background: none;
&:hover {
color: $default-hover-fg-2;
}
}
}
@media screen and (min-width: $tablet) {
&:first-child {
.header {
border-radius: 1em 1em 0 0;
}
}
&:last-child {
.header {
border-radius: 0 0 1em 1em;
}
}
}
.body {
display: flex;
border: $default-border-2;
border-bottom: 0;
box-shadow: $border-shadow-bottom;
}
}
.refresh-button {
position: fixed;
bottom: 1.5em;
right: 1.5em;
button {
width: 4em;
height: 4em;
border-radius: 2em;
background: $refresh-button-bg;
color: $refresh-button-fg;
border: none;
box-shadow: $border-shadow-bottom-right;
&:hover {
color: $default-hover-fg-2;
}
&:disabled {
opacity: .7;
}
}
}
}
</style>

View file

@ -0,0 +1,119 @@
<template>
<div class="switches switchbot-switches">
<Loading v-if="loading" />
<div class="no-content" v-else-if="!Object.keys(devices).length">No Hue lights found.</div>
<Switch :loading="loading" :name="name" :state="device.on" @toggle="toggle(name)"
v-for="(device, name) in devices" :key="name" :has-info="true"
@info="selectedDevice = name; $refs.switchInfoModal.show()" />
<Modal title="Device Info" ref="switchInfoModal">
<div class="switch-info" v-if="selectedDevice">
<div class="row">
<div class="name">Name</div>
<div class="value" v-text="devices[selectedDevice].name" />
</div>
<div class="row">
<div class="name">On</div>
<div class="value" v-text="devices[selectedDevice].on" />
</div>
<div class="row" v-if="devices[selectedDevice].reachable != null">
<div class="name">Reachable</div>
<div class="value" v-text="devices[selectedDevice].reachable" />
</div>
<div class="row" v-if="devices[selectedDevice].bri != null">
<div class="name">Brightness</div>
<div class="value" v-text="devices[selectedDevice].bri" />
</div>
<div class="row" v-if="devices[selectedDevice].ct != null">
<div class="name">Color Temperature</div>
<div class="value" v-text="devices[selectedDevice].ct" />
</div>
<div class="row" v-if="devices[selectedDevice].hue != null">
<div class="name">Hue</div>
<div class="value" v-text="devices[selectedDevice].hue" />
</div>
<div class="row" v-if="devices[selectedDevice].sat != null">
<div class="name">Saturation</div>
<div class="value" v-text="devices[selectedDevice].sat" />
</div>
<div class="row" v-if="devices[selectedDevice].xy != null">
<div class="name">XY</div>
<div class="value" v-text="`[${devices[selectedDevice].xy.join(', ')}]`" />
</div>
<div class="row" v-if="devices[selectedDevice].productname != null">
<div class="name">Product</div>
<div class="value" v-text="devices[selectedDevice].productname" />
</div>
<div class="row" v-if="devices[selectedDevice].manufacturername != null">
<div class="name">Manufacturer</div>
<div class="value" v-text="devices[selectedDevice].manufacturername " />
</div>
<div class="row" v-if="devices[selectedDevice].type != null">
<div class="name">Type</div>
<div class="value" v-text="devices[selectedDevice].type " />
</div>
<div class="row" v-if="devices[selectedDevice].id != null">
<div class="name">ID on network</div>
<div class="value" v-text="devices[selectedDevice].id " />
</div>
<div class="row" v-if="devices[selectedDevice].uniqueid != null">
<div class="name">Unique ID</div>
<div class="value" v-text="devices[selectedDevice].uniqueid " />
</div>
<div class="row" v-if="devices[selectedDevice].swversion != null">
<div class="name">Software version</div>
<div class="value" v-text="devices[selectedDevice].swversion " />
</div>
<div class="row" v-if="devices[selectedDevice].swupdate?.lastinstall">
<div class="name">Last software update</div>
<div class="value" v-text="formatDate(devices[selectedDevice].swupdate.lastinstall, true)" />
</div>
<div class="row" v-if="devices[selectedDevice].swupdate?.state">
<div class="name">Update state</div>
<div class="value" v-text="devices[selectedDevice].swupdate.state" />
</div>
</div>
</Modal>
</div>
</template>
<script>
import Loading from "@/components/Loading";
import SwitchMixin from "@/components/panels/Switches/Mixin";
import Switch from "@/components/panels/Switches/Switch";
import Modal from "@/components/Modal";
export default {
name: "LightHue",
components: {Modal, Switch, Loading},
mixins: [SwitchMixin],
methods: {
async toggle(device) {
const response = await this.request(`${this.pluginName}.toggle`, {lights: [device]})
if (response.success)
this.devices[device].on = !this.devices[device].on
},
}
}
</script>
<style lang="scss" scoped>
@import "../common";
</style>

View file

@ -0,0 +1,81 @@
<script>
import Utils from "@/Utils";
export default {
name: "SwitchesMixin",
mixins: [Utils],
props: {
pluginName: {
type: String,
required: true,
},
bus: {
type: Object,
required: true,
},
config: {
type: Object,
default: () => { return {} },
},
selected: {
type: Boolean,
default: false,
}
},
data() {
return {
loading: false,
initialized: false,
selectedDevice: null,
devices: {},
}
},
methods: {
onRefreshEvent(pluginName) {
if (pluginName !== this.pluginName)
return
this.refresh()
},
async toggle(device) {
const response = await this.request(`${this.pluginName}.toggle`, {device: device})
this.devices[device].on = response.on
},
async refresh() {
this.loading = true
try {
this.devices = (await this.request(`${this.pluginName}.status`)).reduce((obj, device) => {
const name = device.name?.length ? device.name : device.id
obj[name] = device
return obj
}, {})
} finally {
this.loading = false
}
}
},
mounted() {
this.$watch(() => this.selected, (newValue) => {
if (newValue && !this.initialized) {
this.refresh()
this.initialized = true
}
})
this.bus.on('refresh', this.onRefreshEvent)
},
unmounted() {
this.bus.off('refresh', this.onRefreshEvent)
},
}
</script>

View file

@ -0,0 +1,90 @@
<template>
<div class="switch" @click.stop="onToggle">
<Loading v-if="loading" />
<div class="name col-l-10 col-m-9 col-s-8">
<button v-if="hasInfo" @click.prevent="onInfo">
<i class="fa fa-info" />
</button>
<span class="name-content" v-text="name" />
</div>
<div class="toggler col-l-2 col-m-3 col-s-4">
<ToggleSwitch :disabled="loading" :value="state" @input="onToggle" />
</div>
</div>
</template>
<script>
import ToggleSwitch from "@/components/elements/ToggleSwitch";
import Loading from "@/components/Loading";
export default {
name: "Switch",
components: {Loading, ToggleSwitch},
emits: ['toggle', 'info'],
props: {
name: {
type: String,
required: true,
},
state: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
hasInfo: {
type: Boolean,
default: false,
},
},
methods: {
onInfo(event) {
event.stopPropagation()
this.$emit('info')
return false
},
onToggle(event) {
event.stopPropagation()
this.$emit('toggle')
return false
},
}
}
</script>
<style lang="scss" scoped>
.switch {
width: 100%;
display: flex;
position: relative;
align-items: center;
padding: .75em .5em;
border-bottom: $default-border-2;
cursor: pointer;
&:hover {
background: $hover-bg;
}
.toggler {
text-align: right;
}
button {
background: none;
border: none;
&:hover {
color: $default-hover-fg-2;
}
}
}
</style>

View file

@ -0,0 +1,46 @@
<template>
<div class="switches switchbot-switches">
<Loading v-if="loading" />
<div class="no-content" v-else-if="!Object.keys(devices).length">No SwitchBot switches found.</div>
<Switch :loading="loading" :name="name" :state="device.on" @toggle="toggle(name)"
v-for="(device, name) in devices" :key="name" :has-info="true"
@info="selectedDevice = name; $refs.switchInfoModal.show()" />
<Modal title="Device Info" ref="switchInfoModal">
<div class="switch-info" v-if="selectedDevice">
<div class="row">
<div class="name">Name</div>
<div class="value" v-text="devices[selectedDevice].name" />
</div>
<div class="row">
<div class="name">On</div>
<div class="value" v-text="devices[selectedDevice].on" />
</div>
<div class="row">
<div class="name">Address</div>
<div class="value" v-text="devices[selectedDevice].address" />
</div>
</div>
</Modal>
</div>
</template>
<script>
import Loading from "@/components/Loading";
import SwitchMixin from "@/components/panels/Switches/Mixin";
import Switch from "@/components/panels/Switches/Switch";
import Modal from "@/components/Modal";
export default {
name: "SwitchSwitchbot",
components: {Modal, Switch, Loading},
mixins: [SwitchMixin],
}
</script>
<style lang="scss" scoped>
@import "../common";
</style>

View file

@ -0,0 +1,81 @@
<template>
<div class="switches tplink-switches">
<Loading v-if="loading" />
<div class="no-content" v-else-if="!Object.keys(devices).length">No TP-Link switches found.</div>
<Switch :loading="loading" :name="name" :state="device.on" @toggle="toggle(name)"
v-for="(device, name) in devices" :key="name" :has-info="true"
@info="selectedDevice = name; $refs.switchInfoModal.show()" />
<Modal title="Device Info" ref="switchInfoModal">
<div class="switch-info" v-if="selectedDevice">
<div class="row">
<div class="name">Name</div>
<div class="value" v-text="devices[selectedDevice].name" />
</div>
<div class="row">
<div class="name">On</div>
<div class="value" v-text="devices[selectedDevice].on" />
</div>
<div class="row">
<div class="name">IP</div>
<div class="value" v-text="devices[selectedDevice].ip" />
</div>
<div class="row" v-if="devices[selectedDevice].hw_info?.mac">
<div class="name">MAC</div>
<div class="value" v-text="devices[selectedDevice].hw_info.mac" />
</div>
<div class="row" v-if="devices[selectedDevice].current_consumption != null">
<div class="name">Current Consumption</div>
<div class="value" v-text="devices[selectedDevice].current_consumption" />
</div>
<div class="row" v-if="devices[selectedDevice].hw_info?.dev_name">
<div class="name">Device Type</div>
<div class="value" v-text="devices[selectedDevice].hw_info.dev_name" />
</div>
<div class="row" v-if="devices[selectedDevice].hw_info?.fwId">
<div class="name">Firmware ID</div>
<div class="value" v-text="devices[selectedDevice].hw_info.fwId" />
</div>
<div class="row" v-if="devices[selectedDevice].hw_info?.hwId">
<div class="name">Hardware ID</div>
<div class="value" v-text="devices[selectedDevice].hw_info.hwId" />
</div>
<div class="row" v-if="devices[selectedDevice].hw_info?.hw_ver">
<div class="name">Hardware Version</div>
<div class="value" v-text="devices[selectedDevice].hw_info.hw_ver" />
</div>
<div class="row" v-if="devices[selectedDevice].hw_info?.sw_ver">
<div class="name">Software Version</div>
<div class="value" v-text="devices[selectedDevice].hw_info.sw_ver" />
</div>
</div>
</Modal>
</div>
</template>
<script>
import Loading from "@/components/Loading";
import SwitchMixin from "@/components/panels/Switches/Mixin";
import Switch from "@/components/panels/Switches/Switch";
import Modal from "@/components/Modal";
export default {
name: "SwitchTplink",
components: {Modal, Switch, Loading},
mixins: [SwitchMixin],
}
</script>
<style lang="scss" scoped>
@import "../common";
</style>

View file

@ -0,0 +1,46 @@
<template>
<div class="switches wemo-switches">
<Loading v-if="loading" />
<div class="no-content" v-else-if="!Object.keys(devices).length">No WeMo switches found.</div>
<Switch :loading="loading" :name="name" :state="device.on" @toggle="toggle(name)"
v-for="(device, name) in devices" :key="name" :has-info="true"
@info="selectedDevice = name; $refs.switchInfoModal.show()" />
<Modal title="Device Info" ref="switchInfoModal">
<div class="switch-info" v-if="selectedDevice">
<div class="row">
<div class="name">Name</div>
<div class="value" v-text="devices[selectedDevice].name" />
</div>
<div class="row">
<div class="name">On</div>
<div class="value" v-text="devices[selectedDevice].on" />
</div>
<div class="row">
<div class="name">IP</div>
<div class="value" v-text="devices[selectedDevice].ip" />
</div>
</div>
</Modal>
</div>
</template>
<script>
import Loading from "@/components/Loading";
import SwitchMixin from "@/components/panels/Switches/Mixin";
import Switch from "@/components/panels/Switches/Switch";
import Modal from "@/components/Modal";
export default {
name: "SwitchWemo",
components: {Modal, Switch, Loading},
mixins: [SwitchMixin],
}
</script>
<style lang="scss" scoped>
@import "../common";
</style>

View file

@ -0,0 +1,46 @@
<template>
<div class="switches zigbee-mqtt-switches">
<Loading v-if="loading" />
<div class="no-content" v-else-if="!Object.keys(devices).length">No Zigbee switches found.</div>
<Switch :loading="loading" :name="name" :state="device.on" @toggle="toggle(name)"
v-for="(device, name) in devices" :key="name" :has-info="true"
@info="selectedDevice = name; $refs.switchInfoModal.show()" />
<Modal title="Device Info" ref="switchInfoModal">
<div class="switch-info" v-if="selectedDevice">
<!-- <div class="row">-->
<!-- <div class="name">Name</div>-->
<!-- <div class="value" v-text="devices[selectedDevice].name" />-->
<!-- </div>-->
<!-- <div class="row">-->
<!-- <div class="name">On</div>-->
<!-- <div class="value" v-text="devices[selectedDevice].on" />-->
<!-- </div>-->
<!-- <div class="row">-->
<!-- <div class="name">Address</div>-->
<!-- <div class="value" v-text="devices[selectedDevice].address" />-->
<!-- </div>-->
</div>
</Modal>
</div>
</template>
<script>
import Loading from "@/components/Loading";
import SwitchMixin from "@/components/panels/Switches/Mixin";
import Switch from "@/components/panels/Switches/Switch";
import Modal from "@/components/Modal";
export default {
name: "ZigbeeMqtt",
components: {Modal, Switch, Loading},
mixins: [SwitchMixin],
}
</script>
<style lang="scss" scoped>
@import "../common";
</style>

View file

@ -0,0 +1,65 @@
.switches-container {
.switches {
width: 100%;
position: relative;
.no-content {
padding: 1em;
}
.switch-info {
margin: -1em;
padding: 0;
@media screen and (max-width: calc(#{$tablet} - 1px)) {
min-width: calc(100vw - 3em);
}
@media screen and (min-width: $tablet) {
min-width: 45em;
}
}
.row {
width: 100%;
display: flex;
padding: .5em 1em;
&:nth-child(odd) {
background: $background-color;
}
&:nth-child(even) {
background: $default-bg-3;
}
&:hover {
background: $hover-bg;
}
.name {
font-weight: bold;
}
@media screen and (max-width: calc(#{$tablet} - 1px)) {
flex-wrap: wrap;
.name,
.value {
width: 100%;
}
}
@media screen and (min-width: $tablet) {
.name,
.value {
width: 50%;
}
.value {
text-align: right;
}
}
}
}
}

View file

@ -0,0 +1,3 @@
$refresh-button-bg: #182c29;
$refresh-button-fg: white;

View file

@ -1,7 +1,5 @@
<template> <template>
<Loading v-if="loading" /> <Loading v-if="loading" />
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<div id="dashboard" class="columns is-mobile" :class="classes" :style="style"> <div id="dashboard" class="columns is-mobile" :class="classes" :style="style">
<Row v-for="(row, i) in rows" :key="i" :class="row.class" :style="row.style"> <Row v-for="(row, i) in rows" :key="i" :class="row.class" :style="row.style">
@ -118,6 +116,11 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~lato-font/scss/public-api";
$lato-font-path: "~lato-font/fonts";
@include lato-include-font('medium');
#dashboard { #dashboard {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -127,7 +130,7 @@ export default {
padding: 1em 1em 0 1em; padding: 1em 1em 0 1em;
background: $dashboard-bg; background: $dashboard-bg;
background-size: cover; background-size: cover;
font-family: Roboto, Avenir, Helvetica, Arial, sans-serif; font-family: Lato, proxima-nova, Helvetica Neue, Arial, sans-serif;
.blurred { .blurred {
filter: blur(0.075em); filter: blur(0.075em);

View file

@ -84,6 +84,8 @@ export default {
this.request('config.get_procedures'), this.request('config.get_procedures'),
this.request('config.get_device_id'), this.request('config.get_device_id'),
]) ])
this.plugins.switches = {}
}, },
}, },

View file

@ -64,18 +64,19 @@ class BluetoothBlePlugin(SensorPlugin):
if not output: if not output:
return False return False
caps = set(output.pop(0).split('=').pop().strip().split(',')) caps = output[0]
return 'cap_net_raw+eip' in caps and 'cap_net_admin' in caps return ('cap_net_raw+eip' in caps or 'cap_net_raw=eip' in caps) and 'cap_net_admin' in caps
def _check_ble_support(self): def _check_ble_support(self):
# Check if the script is running as root or if the Python executable # Check if the script is running as root or if the Python executable
# has 'cap_net_admin,cap_net_raw+eip' capabilities # has 'cap_net_admin,cap_net_raw+eip' capabilities
exe = self._get_python_interpreter() exe = self._get_python_interpreter()
if os.getuid() != 0 and not self._python_has_ble_capabilities(exe): assert os.getuid() == 0 or self._python_has_ble_capabilities(exe), '''
raise RuntimeError('You are not running platypush as root and the Python interpreter has no ' + You are not running platypush as root and the Python interpreter has no
'capabilities/permissions to access the BLE stack. Set the permissions on ' + capabilities/permissions to access the BLE stack. Set the permissions on
'your Python interpreter through:\n' + your Python interpreter through:
'\t[sudo] setcap "cap_net_raw,cap_net_admin+eip" {}'.format(exe))
[sudo] setcap "cap_net_raw,cap_net_admin+eip" {}'''.format(exe)
@action @action
def scan(self, interface: Optional[str] = None, duration: int = 10) -> BluetoothScanResponse: def scan(self, interface: Optional[str] = None, duration: int = 10) -> BluetoothScanResponse:

View file

@ -1,7 +1,10 @@
from platypush.plugins import Plugin, action from abc import ABC
from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin
class LightPlugin(Plugin): class LightPlugin(SwitchPlugin, ABC):
""" """
Abstract plugin to interface your logic with lights/bulbs. Abstract plugin to interface your logic with lights/bulbs.
""" """
@ -21,10 +24,5 @@ class LightPlugin(Plugin):
""" Toggle the light status (on/off) """ """ Toggle the light status (on/off) """
raise NotImplementedError() raise NotImplementedError()
@action
def status(self, *args, **kwargs):
""" Get the light status """
raise NotImplementedError()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -4,6 +4,7 @@ import time
from enum import Enum from enum import Enum
from threading import Thread, Event from threading import Thread, Event
from typing import List
from platypush.context import get_bus from platypush.context import get_bus
from platypush.message.event.light import LightAnimationStartedEvent, LightAnimationStoppedEvent from platypush.message.event.light import LightAnimationStartedEvent, LightAnimationStoppedEvent
@ -898,8 +899,49 @@ class LightHuePlugin(LightPlugin):
args=(lights,)) args=(lights,))
self.animation_thread.start() self.animation_thread.start()
def status(self): @property
# TODO def switches(self) -> List[dict]:
pass """
:returns: Implements :meth:`platypush.plugins.switch.SwitchPlugin.switches` and returns the status of the
configured lights. Example:
.. code-block:: json
[
{
"id": "3",
"name": "Lightbulb 1",
"on": true,
"bri": 254,
"hue": 1532,
"sat": 215,
"effect": "none",
"xy": [
0.6163,
0.3403
],
"ct": 153,
"alert": "none",
"colormode": "hs",
"reachable": true
"type": "Extended color light",
"modelid": "LCT001",
"manufacturername": "Philips",
"uniqueid": "00:11:22:33:44:55:66:77-88",
"swversion": "5.105.0.21169"
}
]
"""
return [
{
'id': id,
**light.pop('state', {}),
**light,
}
for id, light in self.bridge.get_light().items()
]
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,3 +1,5 @@
from typing import List
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -27,18 +29,37 @@ class SwitchPlugin(Plugin):
@action @action
def status(self, device=None, *args, **kwargs): def status(self, device=None, *args, **kwargs):
""" Get the status of a specified device or of all the configured devices (default)""" """ Get the status of a specified device or of all the configured devices (default)"""
devices = self.devices devices = self.switches
if device: if device:
devices = [d for d in self.devices if d.get('id') == device or d.get('name') == device] devices = [d for d in self.switches if d.get('id') == device or d.get('name') == device]
if devices: if devices:
return self.devices.pop(0) return self.switches.pop(0)
else: else:
return None return None
return devices return devices
@property @property
def devices(self): def switches(self) -> List[dict]:
"""
This property must be implemented by the derived classes and must return a dictionary in the following format:
.. code-block:: json
[
{
"name": "switch_1",
"on": true
},
{
"name": "switch_2",
"on": false
},
]
``name`` and ``on`` are the minimum set of attributes that should be returned for a switch, but more attributes
can also be added.
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,5 +1,6 @@
import enum import enum
import time import time
from typing import List
from platypush.message.response.bluetooth import BluetoothScanResponse from platypush.message.response.bluetooth import BluetoothScanResponse
from platypush.plugins import action from platypush.plugins import action
@ -144,7 +145,7 @@ class SwitchSwitchbotPlugin(SwitchPlugin, BluetoothBlePlugin):
return BluetoothScanResponse(devices=compatible_devices) return BluetoothScanResponse(devices=compatible_devices)
@property @property
def devices(self): def switches(self) -> List[dict]:
return [ return [
{ {
'address': addr, 'address': addr,

View file

@ -148,7 +148,7 @@ class SwitchTplinkPlugin(SwitchPlugin):
} }
@property @property
def devices(self): def switches(self) -> List[dict]:
return [ return [
{ {
'current_consumption': dev.current_consumption(), 'current_consumption': dev.current_consumption(),

View file

@ -1,4 +1,5 @@
import ipaddress import ipaddress
from typing import List
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin from platypush.plugins.switch import SwitchPlugin
@ -48,24 +49,26 @@ class SwitchWemoPlugin(SwitchPlugin):
self._addresses = set(self._devices.values()) self._addresses = set(self._devices.values())
@property @property
def devices(self): def switches(self) -> List[dict]:
""" """
Get the list of available devices Get the list of available devices
:returns: The list of devices. :returns: The list of devices.
Example output:: .. code-block:: json
output = [ [
{ {
"ip": "192.168.1.123", "ip": "192.168.1.123",
"name": "Switch 1", "name": "Switch 1",
"on": true, "on": true
}, },
{ {
# ... "ip": "192.168.1.124",
"name": "Switch 2",
"on": false
} }
] ]
""" """
return [ return [

View file

@ -3,8 +3,10 @@ import threading
import time import time
from platypush.backend.http.utils import HttpUtils from platypush.backend.http.utils import HttpUtils
from platypush.config import Config
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.procedure import Procedure from platypush.procedure import Procedure
from platypush.utils import get_enabled_plugins
class UtilsPlugin(Plugin): class UtilsPlugin(Plugin):
@ -25,6 +27,11 @@ class UtilsPlugin(Plugin):
_pending_timeouts_lock = threading.RLock() _pending_timeouts_lock = threading.RLock()
_pending_intervals_lock = threading.RLock() _pending_intervals_lock = threading.RLock()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._plugins = {}
self._plugins_lock = threading.RLock()
@action @action
def sleep(self, seconds): def sleep(self, seconds):
""" """
@ -89,9 +96,6 @@ class UtilsPlugin(Plugin):
:param name: Name of the timeout to clear :param name: Name of the timeout to clear
:type name: str :type name: str
""" """
timer = None
with self._pending_timeouts_lock: with self._pending_timeouts_lock:
if name not in self._pending_timeouts: if name not in self._pending_timeouts:
self.logger.debug('{} is not a pending timeout'.format(name)) self.logger.debug('{} is not a pending timeout'.format(name))
@ -207,7 +211,6 @@ class UtilsPlugin(Plugin):
return (None, return (None,
"An interval named '{}' is already running".format(name)) "An interval named '{}' is already running".format(name))
procedure = Procedure.build(name=name, requests=actions, _async=False) procedure = Procedure.build(name=name, requests=actions, _async=False)
self._pending_intervals[name] = procedure self._pending_intervals[name] = procedure
@ -233,16 +236,12 @@ class UtilsPlugin(Plugin):
:param name: Name of the interval to clear :param name: Name of the interval to clear
:type name: str :type name: str
""" """
interval = None
with self._pending_intervals_lock: with self._pending_intervals_lock:
if name not in self._pending_intervals: if name not in self._pending_intervals:
self.logger.debug('{} is not a running interval'.format(name)) self.logger.debug('{} is not a running interval'.format(name))
return return
del self._pending_intervals[name] del self._pending_intervals[name]
@action @action
def get_intervals(self): def get_intervals(self):
""" """
@ -311,6 +310,7 @@ class UtilsPlugin(Plugin):
if not timer: if not timer:
return response return response
# noinspection PyProtectedMember
return { return {
name: { name: {
'seconds': timer._args[1], 'seconds': timer._args[1],
@ -328,5 +328,44 @@ class UtilsPlugin(Plugin):
def search_web_directory(self, directory, extensions): def search_web_directory(self, directory, extensions):
return HttpUtils.search_web_directory(directory, *extensions) return HttpUtils.search_web_directory(directory, *extensions)
@action
def get_enabled_plugins(self) -> dict:
"""
:return: The list of enabled plugins as a ``name -> configuration`` map.
"""
if self._plugins:
return self._plugins
plugins = {}
with self._plugins_lock:
for name in get_enabled_plugins().keys():
plugins[name] = Config.get(name)
return plugins
@action
def get_sensor_plugins(self) -> dict:
"""
:return: The list of enabled sensor plugins as a ``name -> configuration`` map.
"""
from platypush.plugins.sensor import SensorPlugin
return {
name: Config.get(name)
for name, plugin in get_enabled_plugins().items()
if isinstance(plugin, SensorPlugin)
}
@action
def get_switch_plugins(self) -> dict:
"""
:return: The list of enabled switch plugins as a ``name -> configuration`` map.
"""
from platypush.plugins.switch import SwitchPlugin
return {
name: Config.get(name)
for name, plugin in get_enabled_plugins().items()
if isinstance(plugin, SwitchPlugin)
}
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,13 +1,15 @@
import json import json
import threading import threading
from queue import Queue
from typing import Optional, List, Any, Dict, Union from typing import Optional, List, Any, Dict, Union
from platypush.message.response import Response from platypush.message.response import Response
from platypush.plugins.mqtt import MqttPlugin, action from platypush.plugins.mqtt import MqttPlugin, action
from platypush.plugins.switch import SwitchPlugin
class ZigbeeMqttPlugin(MqttPlugin): class ZigbeeMqttPlugin(MqttPlugin, SwitchPlugin):
""" """
This plugin allows you to interact with Zigbee devices over MQTT through any Zigbee sniffer and This plugin allows you to interact with Zigbee devices over MQTT through any Zigbee sniffer and
`zigbee2mqtt <https://www.zigbee2mqtt.io/>`_. `zigbee2mqtt <https://www.zigbee2mqtt.io/>`_.
@ -606,6 +608,63 @@ class ZigbeeMqttPlugin(MqttPlugin):
return self.publish(topic=self._topic(device) + '/get', reply_topic=self._topic(device), return self.publish(topic=self._topic(device) + '/get', reply_topic=self._topic(device),
msg=self.build_device_get_request(exposes), **kwargs) msg=self.build_device_get_request(exposes), **kwargs)
@action
def devices_get(self, devices: Optional[List[str]] = None, **kwargs) -> Dict[str, dict]:
"""
Get the properties of the devices connected to the network. *NOTE*: Use this function instead of :meth:`.status`
if you want to retrieve the status of *all* the components associated to the network - :meth:`.status` only
returns the status of the devices with a writable ``ON``/``OFF`` ``state`` property.
:param devices: If set, then only the status of these devices (by friendly name) will be retrieved (default:
retrieve all).
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
(default: query the default configured device).
:return: Key->value map of the device properties:
.. code-block:: json
{
"Bulb": {
"state": "ON",
"brightness": 254
},
"Sensor": {
"temperature": 22.5
}
}
"""
kwargs = self._mqtt_args(**kwargs)
if not devices:
# noinspection PyUnresolvedReferences
devices = set([
device['friendly_name'] or device['ieee_address']
for device in self.devices(**kwargs).output
])
def worker(device: str, q: Queue):
# noinspection PyUnresolvedReferences
q.put(self.device_get(device, **kwargs).output)
queues = {}
workers = {}
response = {}
for device in devices:
queues[device] = Queue()
workers[device] = threading.Thread(target=worker, args=(device, queues[device]))
workers[device].start()
for device in devices:
try:
response[device] = queues[device].get(timeout=kwargs.get('timeout'))
workers[device].join(timeout=kwargs.get('timeout'))
except Exception as e:
self.logger.warning('An error while getting the status of the device {}: {}'.format(
device, str(e)))
return response
# noinspection PyShadowingBuiltins,DuplicatedCode # noinspection PyShadowingBuiltins,DuplicatedCode
@action @action
def device_set(self, device: str, property: str, value: Any, **kwargs): def device_set(self, device: str, property: str, value: Any, **kwargs):
@ -960,8 +1019,10 @@ class ZigbeeMqttPlugin(MqttPlugin):
(default: query the default configured device). (default: query the default configured device).
""" """
return self._parse_response( return self._parse_response(
self.publish(topic=self._topic('bridge/request/group/members/remove{}'.format('_all' if device is None else '')), self.publish(
reply_topic=self._topic('bridge/response/group/members/remove{}'.format('_all' if device is None else '')), topic=self._topic('bridge/request/group/members/remove{}'.format('_all' if device is None else '')),
reply_topic=self._topic(
'bridge/response/group/members/remove{}'.format('_all' if device is None else '')),
msg={ msg={
'group': group, 'group': group,
'device': device, 'device': device,
@ -1005,5 +1066,90 @@ class ZigbeeMqttPlugin(MqttPlugin):
reply_topic=self._topic('bridge/response/device/unbind'), reply_topic=self._topic('bridge/response/device/unbind'),
msg={'from': source, 'to': target}, **self._mqtt_args(**kwargs))) msg={'from': source, 'to': target}, **self._mqtt_args(**kwargs)))
@action
def on(self, device, *args, **kwargs) -> dict:
"""
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.on` and turns on a Zigbee device with a writable
binary property.
"""
switch_info = self._get_switches_info().get(device)
assert switch_info, '{} is not a valid switch'.format(device)
props = self.device_set(device, switch_info['property'], switch_info['value_on']).output
return self._properties_to_switch(device=device, props=props, switch_info=switch_info)
@action
def off(self, device, *args, **kwargs) -> dict:
"""
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.off` and turns off a Zigbee device with a
writable binary property.
"""
switch_info = self._get_switches_info().get(device)
assert switch_info, '{} is not a valid switch'.format(device)
props = self.device_set(device, switch_info['property'], switch_info['value_off']).output
return self._properties_to_switch(device=device, props=props, switch_info=switch_info)
@action
def toggle(self, device, *args, **kwargs) -> dict:
"""
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.toggle` and toggles a Zigbee device with a
writable binary property.
"""
switch_info = self._get_switches_info().get(device)
assert switch_info, '{} is not a valid switch'.format(device)
props = self.device_set(device, switch_info['property'], switch_info['value_toggle']).output
return self._properties_to_switch(device=device, props=props, switch_info=switch_info)
@staticmethod
def _properties_to_switch(device: str, props: dict, switch_info: dict) -> dict:
return {
'on': props[switch_info['property']] == switch_info['value_on'],
'friendly_name': device,
'name': device,
**props,
}
def _get_switches_info(self) -> dict:
def switch_info(device_info: dict) -> dict:
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
for exposed in exposes:
for feature in exposed.get('features', []):
if feature.get('type') == 'binary' and 'value_on' in feature and 'value_off' in feature and \
feature.get('access', 0) & 2:
return {
'property': feature['property'],
'value_on': feature['value_on'],
'value_off': feature['value_off'],
'value_toggle': feature.get('value_toggle', None),
}
return {}
# noinspection PyUnresolvedReferences
devices = self.devices().output
switches_info = {}
for device in devices:
info = switch_info(device)
if not info:
continue
switches_info[device.get('friendly_name', device.get('ieee_address'))] = info
return switches_info
@property
def switches(self) -> List[dict]:
"""
Implements the :class:`platypush.plugins.switch.SwitchPlugin.switches` property and returns the state of any
device on the Zigbee network identified as a switch (a device is identified as a switch if it exposes a writable
``state`` property that can be set to ``ON`` or ``OFF``).
"""
switches_info = self._get_switches_info()
# noinspection PyUnresolvedReferences
return [
self._properties_to_switch(device=name, props=switch, switch_info=switches_info[name])
for name, switch in self.devices_get(list(switches_info.keys())).output.items()
]
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -420,4 +420,20 @@ def get_or_generate_jwt_rsa_key_pair():
return generate_rsa_key_pair(priv_key_file, size=2048) return generate_rsa_key_pair(priv_key_file, size=2048)
def get_enabled_plugins() -> dict:
from platypush.config import Config
from platypush.context import get_plugin
plugins = {}
for name, config in Config.get_plugins().items():
try:
plugin = get_plugin(name)
if plugin:
plugins[name] = plugin
except:
pass
return plugins
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: