Compare commits
15 Commits
3db9c58d31
...
fde834c1b1
Author | SHA1 | Date |
---|---|---|
Fabio Manganiello | fde834c1b1 | |
Fabio Manganiello | 4849e14414 | |
Fabio Manganiello | b8fca97891 | |
Fabio Manganiello | 06dfd1a152 | |
Fabio Manganiello | 64e9bf17cf | |
Fabio Manganiello | 2047b9b76c | |
Fabio Manganiello | 65827aa0cd | |
Fabio Manganiello | b96838a856 | |
Fabio Manganiello | db5846d296 | |
Fabio Manganiello | 0311d87bc3 | |
Fabio Manganiello | de2849546a | |
Fabio Manganiello | a160d3217e | |
Fabio Manganiello | a8fcbef1b5 | |
Fabio Manganiello | b6814b4f16 | |
Fabio Manganiello | 6ef2feea71 |
|
@ -22,3 +22,5 @@ platypush/requests
|
|||
.coverage
|
||||
coverage.xml
|
||||
Session.vim
|
||||
/jsconfig.json
|
||||
/package.json
|
||||
|
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.95bedba1.js"></script><script defer="defer" type="module" src="/static/js/app.d2dd2da4.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.d7cb662c.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.79dede0c.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.c8146c43.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.95bedba1.js"></script><script defer="defer" type="module" src="/static/js/app.c6ad79d3.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.d7cb662c.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.79dede0c.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.0a704e98.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
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
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
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[2380],{6:function(e,n,t){t.d(n,{Z:function(){return g}});var i=t(6252),o=t(3577),a=t(9963),r=function(e){return(0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e},s=["checked"],u=r((function(){return(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1)})),c={class:"label"};function l(e,n,t,r,l,d){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:t.disabled}]),onClick:n[0]||(n[0]=(0,a.iM)((function(){return d.onInput&&d.onInput.apply(d,arguments)}),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:t.value},null,8,s),(0,i._)("label",null,[u,(0,i._)("span",c,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var d={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(this.disabled)return!1;this.$emit("input",e)}}},f=t(3744);const p=(0,f.Z)(d,[["render",l],["__scopeId","data-v-a6396ae8"]]);var g=p},4004:function(e,n,t){t.d(n,{Z:function(){return s}});var i=t(8534),o=(t(1539),t(8309),t(5666),t(6813)),a={name:"SwitchesMixin",mixins:[o.Z],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,n){var t=this;return(0,i.Z)(regeneratorRuntime.mark((function i(){var o;return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return null==n&&(n=e),i.next=3,t.request("".concat(t.pluginName,".toggle"),{device:n});case 3:o=i.sent,t.devices[e].on=o.on;case 5:case"end":return i.stop()}}),i)})))()},refresh:function(){var e=this;return(0,i.Z)(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("".concat(e.pluginName,".switch_status"));case 4:e.devices=n.sent.reduce((function(e,n){var t,i=null!==(t=n.name)&&void 0!==t&&t.length?n.name:n.id;return e[i]=n,e}),{});case 5:return n.prev=5,e.loading=!1,n.finish(5);case 8:case"end":return n.stop()}}),n,null,[[1,,5,8]])})))()}},mounted:function(){var e=this;this.$watch((function(){return e.selected}),(function(n){n&&!e.initialized&&(e.refresh(),e.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted:function(){this.bus.off("refresh",this.onRefreshEvent)}};const r=a;var s=r},8671:function(e,n,t){t.d(n,{Z:function(){return w}});t(8309);var i=t(6252),o=t(9963),a=t(3577),r=function(e){return(0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e},s={class:"name col-l-10 col-m-9 col-s-8"},u=r((function(){return(0,i._)("i",{class:"fa fa-info"},null,-1)})),c=[u],l=["textContent"],d={class:"toggler col-l-2 col-m-3 col-s-4"};function f(e,n,t,r,u,f){var p=(0,i.up)("Loading"),g=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:n[1]||(n[1]=(0,o.iM)((function(){return f.onToggle&&f.onToggle.apply(f,arguments)}),["stop"]))},[t.loading?((0,i.wg)(),(0,i.j4)(p,{key:0})):(0,i.kq)("",!0),(0,i._)("div",s,[t.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:n[0]||(n[0]=(0,o.iM)((function(){return f.onInfo&&f.onInfo.apply(f,arguments)}),["prevent"]))},c)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,a.zw)(t.name)},null,8,l)]),(0,i._)("div",d,[(0,i.Wm)(g,{disabled:t.loading,value:t.state,onInput:f.onToggle},null,8,["disabled","value","onInput"])])])}var p=t(6),g=t(1232),h={name:"Switch",components:{Loading:g.Z,ToggleSwitch:p.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo:function(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle:function(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=t(3744);const m=(0,v.Z)(h,[["render",f],["__scopeId","data-v-38eb9831"]]);var w=m},2380:function(e,n,t){t.r(n),t.d(n,{default:function(){return p}});t(7941);var i=t(6252),o={class:"switches zwave-mqtt-switches"},a={key:1,class:"no-content"};function r(e,n,t,r,s,u){var c=(0,i.up)("Loading"),l=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(c,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",a,"No Z-Wave switches found.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,(function(n,t){return(0,i.wg)(),(0,i.j4)(l,{loading:e.loading,name:t,state:n.on,id:n.id,onToggle:function(i){return e.toggle(t,n.id)},key:t},null,8,["loading","name","state","id","onToggle"])})),128))])}var s=t(1232),u=t(4004),c=t(8671),l={name:"ZwaveMqtt",components:{Switch:c.Z,Loading:s.Z},mixins:[u.Z]},d=t(3744);const f=(0,d.Z)(l,[["render",r],["__scopeId","data-v-c92e52f8"]]);var p=f}}]);
|
||||
//# sourceMappingURL=2380-legacy.0d05fcbd.js.map
|
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[2380],{6:function(e,t,n){n.d(t,{Z:function(){return p}});var i=n(6252),o=n(3577),s=n(9963);const a=e=>((0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e),l=["checked"],d=a((()=>(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1))),c={class:"label"};function u(e,t,n,a,u,r){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:n.disabled}]),onClick:t[0]||(t[0]=(0,s.iM)(((...e)=>r.onInput&&r.onInput(...e)),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:n.value},null,8,l),(0,i._)("label",null,[d,(0,i._)("span",c,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(e){if(this.disabled)return!1;this.$emit("input",e)}}},g=n(3744);const h=(0,g.Z)(r,[["render",u],["__scopeId","data-v-a6396ae8"]]);var p=h},4004:function(e,t,n){n.d(t,{Z:function(){return a}});var i=n(6813),o={name:"SwitchesMixin",mixins:[i.Z],props:{pluginName:{type:String,required:!0},bus:{type:Object,required:!0},config:{type:Object,default:()=>({})},selected:{type:Boolean,default:!1}},data(){return{loading:!1,initialized:!1,selectedDevice:null,devices:{}}},methods:{onRefreshEvent(e){e===this.pluginName&&this.refresh()},async toggle(e,t){null==t&&(t=e);const n=await this.request(`${this.pluginName}.toggle`,{device:t});this.devices[e].on=n.on},async refresh(){this.loading=!0;try{this.devices=(await this.request(`${this.pluginName}.switch_status`)).reduce(((e,t)=>{const n=t.name?.length?t.name:t.id;return e[n]=t,e}),{})}finally{this.loading=!1}}},mounted(){this.$watch((()=>this.selected),(e=>{e&&!this.initialized&&(this.refresh(),this.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted(){this.bus.off("refresh",this.onRefreshEvent)}};const s=o;var a=s},8671:function(e,t,n){n.d(t,{Z:function(){return w}});var i=n(6252),o=n(9963),s=n(3577);const a=e=>((0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e),l={class:"name col-l-10 col-m-9 col-s-8"},d=a((()=>(0,i._)("i",{class:"fa fa-info"},null,-1))),c=[d],u=["textContent"],r={class:"toggler col-l-2 col-m-3 col-s-4"};function g(e,t,n,a,d,g){const h=(0,i.up)("Loading"),p=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:t[1]||(t[1]=(0,o.iM)(((...e)=>g.onToggle&&g.onToggle(...e)),["stop"]))},[n.loading?((0,i.wg)(),(0,i.j4)(h,{key:0})):(0,i.kq)("",!0),(0,i._)("div",l,[n.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:t[0]||(t[0]=(0,o.iM)(((...e)=>g.onInfo&&g.onInfo(...e)),["prevent"]))},c)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,s.zw)(n.name)},null,8,u)]),(0,i._)("div",r,[(0,i.Wm)(p,{disabled:n.loading,value:n.state,onInput:g.onToggle},null,8,["disabled","value","onInput"])])])}var h=n(6),p=n(1232),f={name:"Switch",components:{Loading:p.Z,ToggleSwitch:h.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=n(3744);const m=(0,v.Z)(f,[["render",g],["__scopeId","data-v-38eb9831"]]);var w=m},2380:function(e,t,n){n.r(t),n.d(t,{default:function(){return h}});var i=n(6252);const o={class:"switches zwave-mqtt-switches"},s={key:1,class:"no-content"};function a(e,t,n,a,l,d){const c=(0,i.up)("Loading"),u=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(c,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",s,"No Z-Wave switches found.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,((t,n)=>((0,i.wg)(),(0,i.j4)(u,{loading:e.loading,name:n,state:t.on,id:t.id,onToggle:i=>e.toggle(n,t.id),key:n},null,8,["loading","name","state","id","onToggle"])))),128))])}var l=n(1232),d=n(4004),c=n(8671),u={name:"ZwaveMqtt",components:{Switch:c.Z,Loading:l.Z},mixins:[d.Z]},r=n(3744);const g=(0,r.Z)(u,[["render",a],["__scopeId","data-v-c92e52f8"]]);var h=g}}]);
|
||||
//# sourceMappingURL=2380.292bff03.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because 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
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
|
@ -1,2 +0,0 @@
|
|||
(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[4276],{4276:function(e,n,t){"use strict";t.r(n),t.d(n,{default:function(){return R}});t(7941);var i=t(6252),r=t(3577),s=t(9963),u=function(e){return(0,i.dD)("data-v-eac2ea44"),e=e(),(0,i.Cn)(),e},c={class:"switches-container"},l={class:"switch-plugins"},o={key:0,class:"no-content"},a=["onClick"],d=["textContent"],p={key:0,class:"refresh col-2"},f=["onClick","disabled"],g=u((function(){return(0,i._)("i",{class:"fa fa-sync"},null,-1)})),h=[g],w={class:"refresh-button"},v=["disabled"],m=u((function(){return(0,i._)("i",{class:"fa fa-sync"},null,-1)})),k=[m];function b(e,n,t,u,g,m){var b=(0,i.up)("Loading");return(0,i.wg)(),(0,i.iD)("div",c,[g.loading?((0,i.wg)(),(0,i.j4)(b,{key:0})):(0,i.kq)("",!0),(0,i._)("div",l,[Object.keys(g.plugins).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",o,"No switch plugins configured")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(Object.keys(g.plugins),(function(e){return(0,i.wg)(),(0,i.iD)("div",{class:"switch-plugin",key:e,onClick:function(n){return g.selectedPlugin=g.selectedPlugin===e?null:e}},[(0,i._)("div",{class:(0,r.C_)(["header",{selected:g.selectedPlugin===e}])},[(0,i._)("div",{class:"name col-10",textContent:(0,r.zw)(e)},null,8,d),g.selectedPlugin===e?((0,i.wg)(),(0,i.iD)("div",p,[(0,i._)("button",{onClick:(0,s.iM)((function(n){return g.bus.emit("refresh",e)}),["stop"]),title:"Refresh plugin",disabled:g.loading},h,8,f)])):(0,i.kq)("",!0)],2),(0,i._)("div",{class:(0,r.C_)(["body",{hidden:g.selectedPlugin!==e}])},[((0,i.wg)(),(0,i.j4)((0,i.LL)(g.components[e]),{config:g.plugins[e],"plugin-name":e,selected:g.selectedPlugin===e,bus:g.bus},null,8,["config","plugin-name","selected","bus"]))],2)],8,a)})),128))]),(0,i._)("div",w,[(0,i._)("button",{onClick:n[0]||(n[0]=function(){return m.refresh&&m.refresh.apply(m,arguments)}),disabled:g.loading,title:"Refresh plugins"},k,8,v)])])}var x=t(8534),_=(t(5666),t(1539),t(4747),t(9600),t(1249),t(4916),t(3123),t(7042),t(8783),t(3948),t(1232)),y=t(6813),C=t(9652),I={name:"Switches",components:{Loading:_.Z},mixins:[y.Z],data:function(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:(0,C.Z)()}},methods:{initPanels:function(){var e=this;this.components={},Object.keys(this.plugins).forEach(function(){var n=(0,x.Z)(regeneratorRuntime.mark((function n(r){var s,u,c;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return s=r.split(".").map((function(e){return e[0].toUpperCase()+e.slice(1)})).join(""),u=null,n.prev=2,n.next=5,t(6371)("./".concat(s,"/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:c=(0,i.RC)((0,x.Z)(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]=c,e.components[r]=c;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(0,x.Z)(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()}},P=t(3744);const Z=(0,P.Z)(I,[["render",b],["__scopeId","data-v-eac2ea44"]]);var R=Z},6371:function(e,n,t){var i={"./LightHue/Index":[2844,3490,6590,2844],"./Smartthings/Index":[9196,3490,6590,9196],"./SwitchTplink/Index":[3785,3490,6590,3785],"./SwitchWemo/Index":[5210,3490,6590,5210],"./Switchbot/Index":[9694,3490,6590,9694],"./SwitchbotBluetooth/Index":[9694,3490,6590,9694],"./ZigbeeMqtt/Index":[5466,3490,6590,5466],"./Zwave/Index":[7262,3490,6590,7262],"./ZwaveMqtt/Index":[2380,3490,6590,2380]};function r(e){if(!t.o(i,e))return Promise.resolve().then((function(){var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}));var n=i[e],r=n[0];return Promise.all(n.slice(1).map(t.e)).then((function(){return t(r)}))}r.keys=function(){return Object.keys(i)},r.id=6371,e.exports=r}}]);
|
||||
//# sourceMappingURL=4276-legacy.18787ca7.js.map
|
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[4276],{4276:function(e,n,t){"use strict";t.r(n),t.d(n,{default:function(){return D}});var s=t(6252),i=t(3577),l=t(9963);const c=e=>((0,s.dD)("data-v-eac2ea44"),e=e(),(0,s.Cn)(),e),o={class:"switches-container"},a={class:"switch-plugins"},u={key:0,class:"no-content"},d=["onClick"],r=["textContent"],h={key:0,class:"refresh col-2"},g=["onClick","disabled"],p=c((()=>(0,s._)("i",{class:"fa fa-sync"},null,-1))),f=[p],w={class:"refresh-button"},b=["disabled"],k=c((()=>(0,s._)("i",{class:"fa fa-sync"},null,-1))),m=[k];function v(e,n,t,c,p,k){const v=(0,s.up)("Loading");return(0,s.wg)(),(0,s.iD)("div",o,[p.loading?((0,s.wg)(),(0,s.j4)(v,{key:0})):(0,s.kq)("",!0),(0,s._)("div",a,[Object.keys(p.plugins).length?(0,s.kq)("",!0):((0,s.wg)(),(0,s.iD)("div",u,"No switch plugins configured")),((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(Object.keys(p.plugins),(e=>((0,s.wg)(),(0,s.iD)("div",{class:"switch-plugin",key:e,onClick:n=>p.selectedPlugin=p.selectedPlugin===e?null:e},[(0,s._)("div",{class:(0,i.C_)(["header",{selected:p.selectedPlugin===e}])},[(0,s._)("div",{class:"name col-10",textContent:(0,i.zw)(e)},null,8,r),p.selectedPlugin===e?((0,s.wg)(),(0,s.iD)("div",h,[(0,s._)("button",{onClick:(0,l.iM)((n=>p.bus.emit("refresh",e)),["stop"]),title:"Refresh plugin",disabled:p.loading},f,8,g)])):(0,s.kq)("",!0)],2),(0,s._)("div",{class:(0,i.C_)(["body",{hidden:p.selectedPlugin!==e}])},[((0,s.wg)(),(0,s.j4)((0,s.LL)(p.components[e]),{config:p.plugins[e],"plugin-name":e,selected:p.selectedPlugin===e,bus:p.bus},null,8,["config","plugin-name","selected","bus"]))],2)],8,d)))),128))]),(0,s._)("div",w,[(0,s._)("button",{onClick:n[0]||(n[0]=(...e)=>k.refresh&&k.refresh(...e)),disabled:p.loading,title:"Refresh plugins"},m,8,b)])])}var y=t(1232),_=t(6813),C=t(9652),x={name:"Switches",components:{Loading:y.Z},mixins:[_.Z],data(){return{loading:!1,plugins:{},components:{},selectedPlugin:null,bus:(0,C.Z)()}},methods:{initPanels(){this.components={},Object.keys(this.plugins).forEach((async e=>{const n=e.split(".").map((e=>e[0].toUpperCase()+e.slice(1))).join("");let i=null;try{i=await t(6371)(`./${n}/Index`)}catch(c){return}const l=(0,s.RC)((async()=>i));this.$options.components[e]=l,this.components[e]=l}))},async refresh(){this.loading=!0;try{this.plugins=await this.request("utils.get_switch_plugins"),this.initPanels()}finally{this.loading=!1}}},mounted(){this.refresh()}},I=t(3744);const P=(0,I.Z)(x,[["render",v],["__scopeId","data-v-eac2ea44"]]);var D=P},6371:function(e,n,t){var s={"./LightHue/Index":[2844,3490,6590,2844],"./Smartthings/Index":[9196,3490,6590,9196],"./SwitchTplink/Index":[3785,3490,6590,3785],"./SwitchWemo/Index":[5210,3490,6590,5210],"./Switchbot/Index":[9694,3490,6590,9694],"./SwitchbotBluetooth/Index":[9694,3490,6590,9694],"./ZigbeeMqtt/Index":[5466,3490,6590,5466],"./Zwave/Index":[7262,3490,6590,7262],"./ZwaveMqtt/Index":[2380,3490,6590,2380]};function i(e){if(!t.o(s,e))return Promise.resolve().then((function(){var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}));var n=s[e],i=n[0];return Promise.all(n.slice(1).map(t.e)).then((function(){return t(i)}))}i.keys=function(){return Object.keys(s)},i.id=6371,e.exports=i}}]);
|
||||
//# sourceMappingURL=4276.51717631.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because 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
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[5466],{6:function(e,n,t){t.d(n,{Z:function(){return p}});var i=t(6252),o=t(3577),a=t(9963),r=function(e){return(0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e},s=["checked"],u=r((function(){return(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1)})),c={class:"label"};function l(e,n,t,r,l,d){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:t.disabled}]),onClick:n[0]||(n[0]=(0,a.iM)((function(){return d.onInput&&d.onInput.apply(d,arguments)}),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:t.value},null,8,s),(0,i._)("label",null,[u,(0,i._)("span",c,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var d={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(this.disabled)return!1;this.$emit("input",e)}}},f=t(3744);const g=(0,f.Z)(d,[["render",l],["__scopeId","data-v-a6396ae8"]]);var p=g},4004:function(e,n,t){t.d(n,{Z:function(){return s}});var i=t(8534),o=(t(1539),t(8309),t(5666),t(6813)),a={name:"SwitchesMixin",mixins:[o.Z],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,n){var t=this;return(0,i.Z)(regeneratorRuntime.mark((function i(){var o;return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return null==n&&(n=e),i.next=3,t.request("".concat(t.pluginName,".toggle"),{device:n});case 3:o=i.sent,t.devices[e].on=o.on;case 5:case"end":return i.stop()}}),i)})))()},refresh:function(){var e=this;return(0,i.Z)(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("".concat(e.pluginName,".switch_status"));case 4:e.devices=n.sent.reduce((function(e,n){var t,i=null!==(t=n.name)&&void 0!==t&&t.length?n.name:n.id;return e[i]=n,e}),{});case 5:return n.prev=5,e.loading=!1,n.finish(5);case 8:case"end":return n.stop()}}),n,null,[[1,,5,8]])})))()}},mounted:function(){var e=this;this.$watch((function(){return e.selected}),(function(n){n&&!e.initialized&&(e.refresh(),e.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted:function(){this.bus.off("refresh",this.onRefreshEvent)}};const r=a;var s=r},8671:function(e,n,t){t.d(n,{Z:function(){return w}});t(8309);var i=t(6252),o=t(9963),a=t(3577),r=function(e){return(0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e},s={class:"name col-l-10 col-m-9 col-s-8"},u=r((function(){return(0,i._)("i",{class:"fa fa-info"},null,-1)})),c=[u],l=["textContent"],d={class:"toggler col-l-2 col-m-3 col-s-4"};function f(e,n,t,r,u,f){var g=(0,i.up)("Loading"),p=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:n[1]||(n[1]=(0,o.iM)((function(){return f.onToggle&&f.onToggle.apply(f,arguments)}),["stop"]))},[t.loading?((0,i.wg)(),(0,i.j4)(g,{key:0})):(0,i.kq)("",!0),(0,i._)("div",s,[t.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:n[0]||(n[0]=(0,o.iM)((function(){return f.onInfo&&f.onInfo.apply(f,arguments)}),["prevent"]))},c)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,a.zw)(t.name)},null,8,l)]),(0,i._)("div",d,[(0,i.Wm)(p,{disabled:t.loading,value:t.state,onInput:f.onToggle},null,8,["disabled","value","onInput"])])])}var g=t(6),p=t(1232),h={name:"Switch",components:{Loading:p.Z,ToggleSwitch:g.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo:function(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle:function(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=t(3744);const m=(0,v.Z)(h,[["render",f],["__scopeId","data-v-38eb9831"]]);var w=m},5466:function(e,n,t){t.r(n),t.d(n,{default:function(){return g}});t(7941);var i=t(6252),o={class:"switches zigbee-mqtt-switches"},a={key:1,class:"no-content"};function r(e,n,t,r,s,u){var c=(0,i.up)("Loading"),l=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(c,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",a,"No Zigbee switches found.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,(function(n,t){return(0,i.wg)(),(0,i.j4)(l,{loading:e.loading,name:t,state:n.on,onToggle:function(n){return e.toggle(t)},key:t},null,8,["loading","name","state","onToggle"])})),128))])}var s=t(1232),u=t(4004),c=t(8671),l={name:"ZigbeeMqtt",components:{Switch:c.Z,Loading:s.Z},mixins:[u.Z]},d=t(3744);const f=(0,d.Z)(l,[["render",r],["__scopeId","data-v-33812db1"]]);var g=f}}]);
|
||||
//# sourceMappingURL=5466-legacy.ba464f70.js.map
|
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[5466],{6:function(e,t,n){n.d(t,{Z:function(){return p}});var i=n(6252),o=n(3577),s=n(9963);const a=e=>((0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e),l=["checked"],c=a((()=>(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1))),d={class:"label"};function u(e,t,n,a,u,r){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:n.disabled}]),onClick:t[0]||(t[0]=(0,s.iM)(((...e)=>r.onInput&&r.onInput(...e)),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:n.value},null,8,l),(0,i._)("label",null,[c,(0,i._)("span",d,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(e){if(this.disabled)return!1;this.$emit("input",e)}}},g=n(3744);const h=(0,g.Z)(r,[["render",u],["__scopeId","data-v-a6396ae8"]]);var p=h},4004:function(e,t,n){n.d(t,{Z:function(){return a}});var i=n(6813),o={name:"SwitchesMixin",mixins:[i.Z],props:{pluginName:{type:String,required:!0},bus:{type:Object,required:!0},config:{type:Object,default:()=>({})},selected:{type:Boolean,default:!1}},data(){return{loading:!1,initialized:!1,selectedDevice:null,devices:{}}},methods:{onRefreshEvent(e){e===this.pluginName&&this.refresh()},async toggle(e,t){null==t&&(t=e);const n=await this.request(`${this.pluginName}.toggle`,{device:t});this.devices[e].on=n.on},async refresh(){this.loading=!0;try{this.devices=(await this.request(`${this.pluginName}.switch_status`)).reduce(((e,t)=>{const n=t.name?.length?t.name:t.id;return e[n]=t,e}),{})}finally{this.loading=!1}}},mounted(){this.$watch((()=>this.selected),(e=>{e&&!this.initialized&&(this.refresh(),this.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted(){this.bus.off("refresh",this.onRefreshEvent)}};const s=o;var a=s},8671:function(e,t,n){n.d(t,{Z:function(){return w}});var i=n(6252),o=n(9963),s=n(3577);const a=e=>((0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e),l={class:"name col-l-10 col-m-9 col-s-8"},c=a((()=>(0,i._)("i",{class:"fa fa-info"},null,-1))),d=[c],u=["textContent"],r={class:"toggler col-l-2 col-m-3 col-s-4"};function g(e,t,n,a,c,g){const h=(0,i.up)("Loading"),p=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:t[1]||(t[1]=(0,o.iM)(((...e)=>g.onToggle&&g.onToggle(...e)),["stop"]))},[n.loading?((0,i.wg)(),(0,i.j4)(h,{key:0})):(0,i.kq)("",!0),(0,i._)("div",l,[n.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:t[0]||(t[0]=(0,o.iM)(((...e)=>g.onInfo&&g.onInfo(...e)),["prevent"]))},d)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,s.zw)(n.name)},null,8,u)]),(0,i._)("div",r,[(0,i.Wm)(p,{disabled:n.loading,value:n.state,onInput:g.onToggle},null,8,["disabled","value","onInput"])])])}var h=n(6),p=n(1232),f={name:"Switch",components:{Loading:p.Z,ToggleSwitch:h.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=n(3744);const m=(0,v.Z)(f,[["render",g],["__scopeId","data-v-38eb9831"]]);var w=m},5466:function(e,t,n){n.r(t),n.d(t,{default:function(){return h}});var i=n(6252);const o={class:"switches zigbee-mqtt-switches"},s={key:1,class:"no-content"};function a(e,t,n,a,l,c){const d=(0,i.up)("Loading"),u=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(d,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",s,"No Zigbee switches found.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,((t,n)=>((0,i.wg)(),(0,i.j4)(u,{loading:e.loading,name:n,state:t.on,onToggle:t=>e.toggle(n),key:n},null,8,["loading","name","state","onToggle"])))),128))])}var l=n(1232),c=n(4004),d=n(8671),u={name:"ZigbeeMqtt",components:{Switch:d.Z,Loading:l.Z},mixins:[c.Z]},r=n(3744);const g=(0,r.Z)(u,[["render",a],["__scopeId","data-v-33812db1"]]);var h=g}}]);
|
||||
//# sourceMappingURL=5466.c08dda4e.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because 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
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[7262],{6:function(e,n,t){t.d(n,{Z:function(){return g}});var i=t(6252),o=t(3577),a=t(9963),r=function(e){return(0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e},s=["checked"],u=r((function(){return(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1)})),c={class:"label"};function l(e,n,t,r,l,d){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:t.disabled}]),onClick:n[0]||(n[0]=(0,a.iM)((function(){return d.onInput&&d.onInput.apply(d,arguments)}),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:t.value},null,8,s),(0,i._)("label",null,[u,(0,i._)("span",c,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var d={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(this.disabled)return!1;this.$emit("input",e)}}},f=t(3744);const p=(0,f.Z)(d,[["render",l],["__scopeId","data-v-a6396ae8"]]);var g=p},4004:function(e,n,t){t.d(n,{Z:function(){return s}});var i=t(8534),o=(t(1539),t(8309),t(5666),t(6813)),a={name:"SwitchesMixin",mixins:[o.Z],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,n){var t=this;return(0,i.Z)(regeneratorRuntime.mark((function i(){var o;return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return null==n&&(n=e),i.next=3,t.request("".concat(t.pluginName,".toggle"),{device:n});case 3:o=i.sent,t.devices[e].on=o.on;case 5:case"end":return i.stop()}}),i)})))()},refresh:function(){var e=this;return(0,i.Z)(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("".concat(e.pluginName,".switch_status"));case 4:e.devices=n.sent.reduce((function(e,n){var t,i=null!==(t=n.name)&&void 0!==t&&t.length?n.name:n.id;return e[i]=n,e}),{});case 5:return n.prev=5,e.loading=!1,n.finish(5);case 8:case"end":return n.stop()}}),n,null,[[1,,5,8]])})))()}},mounted:function(){var e=this;this.$watch((function(){return e.selected}),(function(n){n&&!e.initialized&&(e.refresh(),e.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted:function(){this.bus.off("refresh",this.onRefreshEvent)}};const r=a;var s=r},8671:function(e,n,t){t.d(n,{Z:function(){return w}});t(8309);var i=t(6252),o=t(9963),a=t(3577),r=function(e){return(0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e},s={class:"name col-l-10 col-m-9 col-s-8"},u=r((function(){return(0,i._)("i",{class:"fa fa-info"},null,-1)})),c=[u],l=["textContent"],d={class:"toggler col-l-2 col-m-3 col-s-4"};function f(e,n,t,r,u,f){var p=(0,i.up)("Loading"),g=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:n[1]||(n[1]=(0,o.iM)((function(){return f.onToggle&&f.onToggle.apply(f,arguments)}),["stop"]))},[t.loading?((0,i.wg)(),(0,i.j4)(p,{key:0})):(0,i.kq)("",!0),(0,i._)("div",s,[t.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:n[0]||(n[0]=(0,o.iM)((function(){return f.onInfo&&f.onInfo.apply(f,arguments)}),["prevent"]))},c)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,a.zw)(t.name)},null,8,l)]),(0,i._)("div",d,[(0,i.Wm)(g,{disabled:t.loading,value:t.state,onInput:f.onToggle},null,8,["disabled","value","onInput"])])])}var p=t(6),g=t(1232),h={name:"Switch",components:{Loading:g.Z,ToggleSwitch:p.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo:function(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle:function(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=t(3744);const m=(0,v.Z)(h,[["render",f],["__scopeId","data-v-38eb9831"]]);var w=m},7262:function(e,n,t){t.r(n),t.d(n,{default:function(){return p}});t(7941);var i=t(6252),o={class:"switches zwave-switches"},a={key:1,class:"no-content"};function r(e,n,t,r,s,u){var c=(0,i.up)("Loading"),l=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(c,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",a,"No Z-Wave switches found.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,(function(n,t){return(0,i.wg)(),(0,i.j4)(l,{loading:e.loading,name:t,state:n.on,id:n.id,onToggle:function(i){return e.toggle(t,n.id)},key:t},null,8,["loading","name","state","id","onToggle"])})),128))])}var s=t(1232),u=t(4004),c=t(8671),l={name:"Zwave",components:{Switch:c.Z,Loading:s.Z},mixins:[u.Z]},d=t(3744);const f=(0,d.Z)(l,[["render",r],["__scopeId","data-v-6aa1e625"]]);var p=f}}]);
|
||||
//# sourceMappingURL=7262-legacy.13af887b.js.map
|
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[7262],{6:function(e,t,n){n.d(t,{Z:function(){return p}});var i=n(6252),o=n(3577),s=n(9963);const a=e=>((0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e),l=["checked"],d=a((()=>(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1))),c={class:"label"};function u(e,t,n,a,u,r){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:n.disabled}]),onClick:t[0]||(t[0]=(0,s.iM)(((...e)=>r.onInput&&r.onInput(...e)),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:n.value},null,8,l),(0,i._)("label",null,[d,(0,i._)("span",c,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(e){if(this.disabled)return!1;this.$emit("input",e)}}},g=n(3744);const h=(0,g.Z)(r,[["render",u],["__scopeId","data-v-a6396ae8"]]);var p=h},4004:function(e,t,n){n.d(t,{Z:function(){return a}});var i=n(6813),o={name:"SwitchesMixin",mixins:[i.Z],props:{pluginName:{type:String,required:!0},bus:{type:Object,required:!0},config:{type:Object,default:()=>({})},selected:{type:Boolean,default:!1}},data(){return{loading:!1,initialized:!1,selectedDevice:null,devices:{}}},methods:{onRefreshEvent(e){e===this.pluginName&&this.refresh()},async toggle(e,t){null==t&&(t=e);const n=await this.request(`${this.pluginName}.toggle`,{device:t});this.devices[e].on=n.on},async refresh(){this.loading=!0;try{this.devices=(await this.request(`${this.pluginName}.switch_status`)).reduce(((e,t)=>{const n=t.name?.length?t.name:t.id;return e[n]=t,e}),{})}finally{this.loading=!1}}},mounted(){this.$watch((()=>this.selected),(e=>{e&&!this.initialized&&(this.refresh(),this.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted(){this.bus.off("refresh",this.onRefreshEvent)}};const s=o;var a=s},8671:function(e,t,n){n.d(t,{Z:function(){return w}});var i=n(6252),o=n(9963),s=n(3577);const a=e=>((0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e),l={class:"name col-l-10 col-m-9 col-s-8"},d=a((()=>(0,i._)("i",{class:"fa fa-info"},null,-1))),c=[d],u=["textContent"],r={class:"toggler col-l-2 col-m-3 col-s-4"};function g(e,t,n,a,d,g){const h=(0,i.up)("Loading"),p=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:t[1]||(t[1]=(0,o.iM)(((...e)=>g.onToggle&&g.onToggle(...e)),["stop"]))},[n.loading?((0,i.wg)(),(0,i.j4)(h,{key:0})):(0,i.kq)("",!0),(0,i._)("div",l,[n.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:t[0]||(t[0]=(0,o.iM)(((...e)=>g.onInfo&&g.onInfo(...e)),["prevent"]))},c)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,s.zw)(n.name)},null,8,u)]),(0,i._)("div",r,[(0,i.Wm)(p,{disabled:n.loading,value:n.state,onInput:g.onToggle},null,8,["disabled","value","onInput"])])])}var h=n(6),p=n(1232),f={name:"Switch",components:{Loading:p.Z,ToggleSwitch:h.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=n(3744);const m=(0,v.Z)(f,[["render",g],["__scopeId","data-v-38eb9831"]]);var w=m},7262:function(e,t,n){n.r(t),n.d(t,{default:function(){return h}});var i=n(6252);const o={class:"switches zwave-switches"},s={key:1,class:"no-content"};function a(e,t,n,a,l,d){const c=(0,i.up)("Loading"),u=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(c,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",s,"No Z-Wave switches found.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,((t,n)=>((0,i.wg)(),(0,i.j4)(u,{loading:e.loading,name:n,state:t.on,id:t.id,onToggle:i=>e.toggle(n,t.id),key:n},null,8,["loading","name","state","id","onToggle"])))),128))])}var l=n(1232),d=n(4004),c=n(8671),u={name:"Zwave",components:{Switch:c.Z,Loading:l.Z},mixins:[d.Z]},r=n(3744);const g=(0,r.Z)(u,[["render",a],["__scopeId","data-v-6aa1e625"]]);var h=g}}]);
|
||||
//# sourceMappingURL=7262.6193bf34.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[9196],{6:function(e,t,n){n.d(t,{Z:function(){return p}});var i=n(6252),o=n(3577),s=n(9963);const a=e=>((0,i.dD)("data-v-a6396ae8"),e=e(),(0,i.Cn)(),e),l=["checked"],c=a((()=>(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1))),d={class:"label"};function u(e,t,n,a,u,r){return(0,i.wg)(),(0,i.iD)("div",{class:(0,o.C_)(["power-switch",{disabled:n.disabled}]),onClick:t[0]||(t[0]=(0,s.iM)(((...e)=>r.onInput&&r.onInput(...e)),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:n.value},null,8,l),(0,i._)("label",null,[c,(0,i._)("span",d,[(0,i.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(e){if(this.disabled)return!1;this.$emit("input",e)}}},g=n(3744);const h=(0,g.Z)(r,[["render",u],["__scopeId","data-v-a6396ae8"]]);var p=h},4004:function(e,t,n){n.d(t,{Z:function(){return a}});var i=n(6813),o={name:"SwitchesMixin",mixins:[i.Z],props:{pluginName:{type:String,required:!0},bus:{type:Object,required:!0},config:{type:Object,default:()=>({})},selected:{type:Boolean,default:!1}},data(){return{loading:!1,initialized:!1,selectedDevice:null,devices:{}}},methods:{onRefreshEvent(e){e===this.pluginName&&this.refresh()},async toggle(e,t){null==t&&(t=e);const n=await this.request(`${this.pluginName}.toggle`,{device:t});this.devices[e].on=n.on},async refresh(){this.loading=!0;try{this.devices=(await this.request(`${this.pluginName}.switch_status`)).reduce(((e,t)=>{const n=t.name?.length?t.name:t.id;return e[n]=t,e}),{})}finally{this.loading=!1}}},mounted(){this.$watch((()=>this.selected),(e=>{e&&!this.initialized&&(this.refresh(),this.initialized=!0)})),this.bus.on("refresh",this.onRefreshEvent)},unmounted(){this.bus.off("refresh",this.onRefreshEvent)}};const s=o;var a=s},9196:function(e,t,n){n.r(t),n.d(t,{default:function(){return h}});var i=n(6252);const o={class:"switches smartthings-switches"},s={key:1,class:"no-content"};function a(e,t,n,a,l,c){const d=(0,i.up)("Loading"),u=(0,i.up)("Switch");return(0,i.wg)(),(0,i.iD)("div",o,[e.loading?((0,i.wg)(),(0,i.j4)(d,{key:0})):Object.keys(e.devices).length?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",s,"No switches found on SmartThings.")),((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.devices,((t,n)=>((0,i.wg)(),(0,i.j4)(u,{loading:e.loading,name:n,state:t.on,onToggle:t=>e.toggle(n),key:n,"has-info":!0,onInfo:t=>{e.selectedDevice=n,e.$refs.switchInfoModal.show()}},null,8,["loading","name","state","onToggle","onInfo"])))),128))])}var l=n(1232),c=n(4004),d=n(8671),u={name:"Smartthings",components:{Switch:d.Z,Loading:l.Z},mixins:[c.Z]},r=n(3744);const g=(0,r.Z)(u,[["render",a],["__scopeId","data-v-7cc9c062"]]);var h=g},8671:function(e,t,n){n.d(t,{Z:function(){return w}});var i=n(6252),o=n(9963),s=n(3577);const a=e=>((0,i.dD)("data-v-38eb9831"),e=e(),(0,i.Cn)(),e),l={class:"name col-l-10 col-m-9 col-s-8"},c=a((()=>(0,i._)("i",{class:"fa fa-info"},null,-1))),d=[c],u=["textContent"],r={class:"toggler col-l-2 col-m-3 col-s-4"};function g(e,t,n,a,c,g){const h=(0,i.up)("Loading"),p=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",{class:"switch",onClick:t[1]||(t[1]=(0,o.iM)(((...e)=>g.onToggle&&g.onToggle(...e)),["stop"]))},[n.loading?((0,i.wg)(),(0,i.j4)(h,{key:0})):(0,i.kq)("",!0),(0,i._)("div",l,[n.hasInfo?((0,i.wg)(),(0,i.iD)("button",{key:0,onClick:t[0]||(t[0]=(0,o.iM)(((...e)=>g.onInfo&&g.onInfo(...e)),["prevent"]))},d)):(0,i.kq)("",!0),(0,i._)("span",{class:"name-content",textContent:(0,s.zw)(n.name)},null,8,u)]),(0,i._)("div",r,[(0,i.Wm)(p,{disabled:n.loading,value:n.state,onInput:g.onToggle},null,8,["disabled","value","onInput"])])])}var h=n(6),p=n(1232),f={name:"Switch",components:{Loading:p.Z,ToggleSwitch:h.Z},emits:["toggle","info"],props:{name:{type:String,required:!0},state:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},hasInfo:{type:Boolean,default:!1},id:{type:String}},methods:{onInfo(e){return e.stopPropagation(),this.$emit("info"),!1},onToggle(e){return e.stopPropagation(),this.$emit("toggle"),!1}}},v=n(3744);const m=(0,v.Z)(f,[["render",g],["__scopeId","data-v-38eb9831"]]);var w=m}}]);
|
||||
//# sourceMappingURL=9196.462b659b.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because 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
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
|
@ -8,11 +8,11 @@
|
|||
:error="error" />
|
||||
</div>
|
||||
|
||||
<div class="col-s-8 col-m-9 label">
|
||||
<div class="col-s-7 col-m-8 label">
|
||||
<div class="name" v-text="value.name" />
|
||||
</div>
|
||||
|
||||
<div class="col-s-3 col-m-2 buttons pull-right">
|
||||
<div class="col-s-4 col-m-3 buttons pull-right">
|
||||
<button @click.stop="collapsed = !collapsed">
|
||||
<i class="fas"
|
||||
:class="{'fa-angle-up': !collapsed, 'fa-angle-down': collapsed}" />
|
||||
|
@ -59,7 +59,10 @@ export default {
|
|||
if (this.value?.is_write_only || this.value?.value == null)
|
||||
return null
|
||||
|
||||
return this.value.value
|
||||
let value = this.value.value
|
||||
if (this.value.unit)
|
||||
value = `${value} ${this.value.unit}`
|
||||
return value
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -102,6 +105,7 @@ export default {
|
|||
.value-percent {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
direction: ltr;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,214 +0,0 @@
|
|||
<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>
|
|
@ -1,119 +0,0 @@
|
|||
<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>
|
|
@ -1,84 +0,0 @@
|
|||
<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, id) {
|
||||
if (id == null)
|
||||
id = device
|
||||
|
||||
const response = await this.request(`${this.pluginName}.toggle`, {device: id})
|
||||
this.devices[device].on = response.on
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
try {
|
||||
this.devices = (await this.request(`${this.pluginName}.switch_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>
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<div class="switches smartthings-switches">
|
||||
<Loading v-if="loading" />
|
||||
<div class="no-content" v-else-if="!Object.keys(devices).length">No switches found on SmartThings.</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()" />
|
||||
</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: "Smartthings",
|
||||
components: {Switch, Loading},
|
||||
mixins: [SwitchMixin],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../common";
|
||||
</style>
|
|
@ -1,94 +0,0 @@
|
|||
<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,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
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>
|
|
@ -1,81 +0,0 @@
|
|||
<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>
|
|
@ -1,46 +0,0 @@
|
|||
<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>
|
|
@ -1 +0,0 @@
|
|||
SwitchbotBluetooth
|
|
@ -1,46 +0,0 @@
|
|||
<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: "SwitchbotBluetooth",
|
||||
components: {Modal, Switch, Loading},
|
||||
mixins: [SwitchMixin],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../common";
|
||||
</style>
|
|
@ -1,25 +0,0 @@
|
|||
<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" />
|
||||
</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: "ZigbeeMqtt",
|
||||
components: {Switch, Loading},
|
||||
mixins: [SwitchMixin],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../common";
|
||||
</style>
|
|
@ -1,25 +0,0 @@
|
|||
<template>
|
||||
<div class="switches zwave-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: "Zwave",
|
||||
components: {Switch, Loading},
|
||||
mixins: [SwitchMixin],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../common";
|
||||
</style>
|
|
@ -1,25 +0,0 @@
|
|||
<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>
|
|
@ -1,65 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
$refresh-button-bg: #182c29;
|
||||
$refresh-button-fg: white;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from redis import Redis
|
||||
|
||||
|
@ -16,7 +16,7 @@ class RedisBackend(Backend):
|
|||
and can't post events or requests to the application bus.
|
||||
"""
|
||||
|
||||
def __init__(self, queue='platypush_bus_mq', redis_args=None, *args, **kwargs):
|
||||
def __init__(self, *args, queue='platypush_bus_mq', redis_args=None, **kwargs):
|
||||
"""
|
||||
:param queue: Queue name to listen on (default: ``platypush_bus_mq``)
|
||||
:type queue: str
|
||||
|
@ -40,12 +40,21 @@ class RedisBackend(Backend):
|
|||
self.redis_args = redis_args
|
||||
self.redis: Optional[Redis] = None
|
||||
|
||||
def send_message(self, msg, queue_name=None, **kwargs):
|
||||
msg = str(msg)
|
||||
if queue_name:
|
||||
self.redis.rpush(queue_name, msg)
|
||||
else:
|
||||
self.redis.rpush(self.queue, msg)
|
||||
def send_message(
|
||||
self, msg: Union[str, Message], queue_name: Optional[str] = None, **_
|
||||
):
|
||||
"""
|
||||
Send a message to a Redis queue.
|
||||
|
||||
:param msg: Message to send, as a ``Message`` object or a string.
|
||||
:param queue_name: Queue name to send the message to (default: ``platypush_bus_mq``).
|
||||
"""
|
||||
|
||||
if not self.redis:
|
||||
self.logger.warning('The Redis backend is not yet running.')
|
||||
return
|
||||
|
||||
self.redis.rpush(queue_name or self.queue, str(msg))
|
||||
|
||||
def get_message(self, queue_name=None):
|
||||
queue = queue_name or self.queue
|
||||
|
@ -60,6 +69,7 @@ class RedisBackend(Backend):
|
|||
self.logger.debug(str(e))
|
||||
try:
|
||||
import ast
|
||||
|
||||
msg = Message.build(ast.literal_eval(msg))
|
||||
except Exception as ee:
|
||||
self.logger.debug(str(ee))
|
||||
|
@ -72,7 +82,11 @@ class RedisBackend(Backend):
|
|||
|
||||
def run(self):
|
||||
super().run()
|
||||
self.logger.info('Initialized Redis backend on queue {} with arguments {}'.format(self.queue, self.redis_args))
|
||||
self.logger.info(
|
||||
'Initialized Redis backend on queue %s with arguments %s',
|
||||
self.queue,
|
||||
self.redis_args,
|
||||
)
|
||||
|
||||
with Redis(**self.redis_args) as self.redis:
|
||||
while not self.should_stop():
|
||||
|
@ -81,7 +95,7 @@ class RedisBackend(Backend):
|
|||
if not msg:
|
||||
continue
|
||||
|
||||
self.logger.info('Received message on the Redis backend: {}'.format(msg))
|
||||
self.logger.info('Received message on the Redis backend: %s', msg)
|
||||
self.on_message(msg)
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
|
|
|
@ -1,48 +1,55 @@
|
|||
import logging
|
||||
import threading
|
||||
|
||||
from redis import Redis
|
||||
from typing import Optional
|
||||
|
||||
from platypush.bus import Bus
|
||||
from platypush.config import Config
|
||||
from platypush.message import Message
|
||||
|
||||
logger = logging.getLogger('platypush:bus:redis')
|
||||
|
||||
|
||||
class RedisBus(Bus):
|
||||
""" Overrides the in-process in-memory local bus with a Redis bus """
|
||||
"""
|
||||
Overrides the in-process in-memory local bus with a Redis bus
|
||||
"""
|
||||
|
||||
_DEFAULT_REDIS_QUEUE = 'platypush/bus'
|
||||
|
||||
def __init__(self, *args, on_message=None, redis_queue=None, **kwargs):
|
||||
from platypush.utils import get_redis
|
||||
|
||||
super().__init__(on_message=on_message)
|
||||
|
||||
if not args and not kwargs:
|
||||
kwargs = (Config.get('backend.redis') or {}).get('redis_args', {})
|
||||
|
||||
self.redis = Redis(*args, **kwargs)
|
||||
self.redis = get_redis(*args, **kwargs)
|
||||
self.redis_args = kwargs
|
||||
self.redis_queue = redis_queue or self._DEFAULT_REDIS_QUEUE
|
||||
self.on_message = on_message
|
||||
self.thread_id = threading.get_ident()
|
||||
|
||||
def get(self):
|
||||
""" Reads one message from the Redis queue """
|
||||
def get(self) -> Optional[Message]:
|
||||
"""
|
||||
Reads one message from the Redis queue
|
||||
"""
|
||||
|
||||
try:
|
||||
if self.should_stop():
|
||||
return
|
||||
return None
|
||||
|
||||
msg = self.redis.blpop(self.redis_queue, timeout=1)
|
||||
if not msg or msg[1] is None:
|
||||
return
|
||||
return None
|
||||
|
||||
msg = msg[1].decode('utf-8')
|
||||
return Message.build(msg)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
return None
|
||||
|
||||
def post(self, msg):
|
||||
""" Sends a message to the Redis queue """
|
||||
"""
|
||||
Sends a message to the Redis queue
|
||||
"""
|
||||
|
||||
return self.redis.rpush(self.redis_queue, str(msg))
|
||||
|
||||
def stop(self):
|
||||
|
|
|
@ -11,7 +11,7 @@ import shutil
|
|||
import socket
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
from typing import Optional
|
||||
from typing import Optional, Set
|
||||
|
||||
import yaml
|
||||
|
||||
|
@ -22,9 +22,6 @@ from platypush.utils import (
|
|||
is_functional_cron,
|
||||
)
|
||||
|
||||
""" Config singleton instance """
|
||||
_default_config_instance = None
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
|
@ -38,16 +35,17 @@ class Config:
|
|||
Config.get('foo')
|
||||
"""
|
||||
|
||||
"""
|
||||
Default config file locations:
|
||||
- $HOME/.config/platypush/config.yaml
|
||||
- /etc/platypush/config.yaml
|
||||
"""
|
||||
# Default config file locations:
|
||||
# - $HOME/.config/platypush/config.yaml
|
||||
# - /etc/platypush/config.yaml
|
||||
_cfgfile_locations = [
|
||||
os.path.join(os.path.expanduser('~'), '.config', 'platypush', 'config.yaml'),
|
||||
os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'),
|
||||
]
|
||||
|
||||
# Config singleton instance
|
||||
_instance = None
|
||||
|
||||
_default_constants = {
|
||||
'today': datetime.date.today,
|
||||
'now': datetime.datetime.now,
|
||||
|
@ -56,7 +54,7 @@ class Config:
|
|||
_workdir_location = os.path.join(
|
||||
os.path.expanduser('~'), '.local', 'share', 'platypush'
|
||||
)
|
||||
_included_files = set()
|
||||
_included_files: Set[str] = set()
|
||||
|
||||
def __init__(self, cfgfile=None):
|
||||
"""
|
||||
|
@ -118,9 +116,8 @@ class Config:
|
|||
}
|
||||
else:
|
||||
db_engine = {
|
||||
'engine': 'sqlite:///' + os.path.join(
|
||||
quote(self._config['workdir']), 'main.db'
|
||||
)
|
||||
'engine': 'sqlite:///'
|
||||
+ os.path.join(quote(self._config['workdir']), 'main.db')
|
||||
}
|
||||
|
||||
self._config['db'] = db_engine
|
||||
|
@ -139,11 +136,7 @@ class Config:
|
|||
try:
|
||||
os.makedirs(logdir, exist_ok=True)
|
||||
except Exception as e:
|
||||
print(
|
||||
'Unable to create logs directory {}: {}'.format(
|
||||
logdir, str(e)
|
||||
)
|
||||
)
|
||||
print(f'Unable to create logs directory {logdir}: {e}')
|
||||
|
||||
v = logfile
|
||||
del logging_config['stream']
|
||||
|
@ -214,11 +207,9 @@ class Config:
|
|||
continue
|
||||
if not os.path.isabs(include_file):
|
||||
include_file = os.path.join(cfgfile_dir, include_file)
|
||||
self._included_files.add(include_file)
|
||||
|
||||
included_config = self._read_config_file(include_file)
|
||||
for incl_section in included_config.keys():
|
||||
config[incl_section] = included_config[incl_section]
|
||||
self._included_files.add(include_file)
|
||||
config.update(self._read_config_file(include_file))
|
||||
elif section == 'scripts_dir':
|
||||
assert isinstance(file_config[section], str)
|
||||
config['scripts_dir'] = os.path.abspath(
|
||||
|
@ -237,11 +228,7 @@ class Config:
|
|||
try:
|
||||
module = importlib.import_module(modname)
|
||||
except Exception as e:
|
||||
print(
|
||||
'Unhandled exception while importing module {}: {}'.format(
|
||||
modname, str(e)
|
||||
)
|
||||
)
|
||||
print(f'Unhandled exception while importing module {modname}: {e}')
|
||||
return
|
||||
|
||||
prefix = modname + '.' if prefix is None else prefix
|
||||
|
@ -284,19 +271,19 @@ class Config:
|
|||
sys.path = sys_path
|
||||
|
||||
def _init_components(self):
|
||||
for key in self._config.keys():
|
||||
for key, component in self._config.items():
|
||||
if (
|
||||
key.startswith('backend.')
|
||||
and '.'.join(key.split('.')[1:]) in self._backend_manifests
|
||||
):
|
||||
backend_name = '.'.join(key.split('.')[1:])
|
||||
self.backends[backend_name] = self._config[key]
|
||||
self.backends[backend_name] = component
|
||||
elif key.startswith('event.hook.'):
|
||||
hook_name = '.'.join(key.split('.')[2:])
|
||||
self.event_hooks[hook_name] = self._config[key]
|
||||
self.event_hooks[hook_name] = component
|
||||
elif key.startswith('cron.'):
|
||||
cron_name = '.'.join(key.split('.')[1:])
|
||||
self.cronjobs[cron_name] = self._config[key]
|
||||
self.cronjobs[cron_name] = component
|
||||
elif key.startswith('procedure.'):
|
||||
tokens = key.split('.')
|
||||
_async = bool(len(tokens) > 2 and tokens[1] == 'async')
|
||||
|
@ -314,11 +301,11 @@ class Config:
|
|||
|
||||
self.procedures[procedure_name] = {
|
||||
'_async': _async,
|
||||
'actions': self._config[key],
|
||||
'actions': component,
|
||||
'args': args,
|
||||
}
|
||||
elif key in self._plugin_manifests:
|
||||
self.plugins[key] = self._config[key]
|
||||
self.plugins[key] = component
|
||||
|
||||
def _init_manifests(self, base_dir: Optional[str] = None):
|
||||
if not base_dir:
|
||||
|
@ -353,7 +340,7 @@ class Config:
|
|||
assert dashboards_dir
|
||||
abspath = os.path.join(dashboards_dir, name + '.xml')
|
||||
if not os.path.isfile(abspath):
|
||||
return
|
||||
return None
|
||||
|
||||
with open(abspath, 'r') as fp:
|
||||
return fp.read()
|
||||
|
@ -373,111 +360,94 @@ class Config:
|
|||
|
||||
return dashboards
|
||||
|
||||
@staticmethod
|
||||
def get_dashboard(name: str, dashboards_dir: Optional[str] = None) -> Optional[str]:
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance._get_dashboard(name, dashboards_dir)
|
||||
@classmethod
|
||||
def _get_instance(
|
||||
cls, cfgfile: Optional[str] = None, force_reload: bool = False
|
||||
) -> "Config":
|
||||
"""
|
||||
Lazy getter/setter for the default configuration instance.
|
||||
"""
|
||||
if force_reload or cls._instance is None:
|
||||
cfg_args = [cfgfile] if cfgfile else []
|
||||
cls._instance = Config(*cfg_args)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_dashboard(
|
||||
cls, name: str, dashboards_dir: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
# pylint: disable=protected-access
|
||||
return cls._get_instance()._get_dashboard(name, dashboards_dir)
|
||||
|
||||
@classmethod
|
||||
def get_dashboards(cls, dashboards_dir: Optional[str] = None) -> dict:
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance._get_dashboards(dashboards_dir)
|
||||
# pylint: disable=protected-access
|
||||
return cls._get_instance()._get_dashboards(dashboards_dir)
|
||||
|
||||
def _init_dashboards(self, dashboards_dir: str):
|
||||
self.dashboards = self._get_dashboards(dashboards_dir)
|
||||
|
||||
@staticmethod
|
||||
def get_backends():
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance.backends
|
||||
|
||||
@staticmethod
|
||||
def get_plugins():
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance.plugins
|
||||
|
||||
@staticmethod
|
||||
def get_event_hooks():
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance.event_hooks
|
||||
|
||||
@staticmethod
|
||||
def get_procedures():
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance.procedures
|
||||
|
||||
@staticmethod
|
||||
def get_constants():
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
constants = {}
|
||||
|
||||
for name in _default_config_instance.constants.keys():
|
||||
constants[name] = Config.get_constant(name)
|
||||
return constants
|
||||
|
||||
@staticmethod
|
||||
def get_constant(name):
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
|
||||
if name not in _default_config_instance.constants:
|
||||
return None
|
||||
value = _default_config_instance.constants[name]
|
||||
return value() if callable(value) else value
|
||||
|
||||
@staticmethod
|
||||
def get_cronjobs():
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
return _default_config_instance.cronjobs
|
||||
@classmethod
|
||||
def get_backends(cls):
|
||||
return cls._get_instance().backends
|
||||
|
||||
@classmethod
|
||||
def _get_default_cfgfile(cls):
|
||||
def get_plugins(cls):
|
||||
return cls._get_instance().plugins
|
||||
|
||||
@classmethod
|
||||
def get_event_hooks(cls):
|
||||
return cls._get_instance().event_hooks
|
||||
|
||||
@classmethod
|
||||
def get_procedures(cls):
|
||||
return cls._get_instance().procedures
|
||||
|
||||
@classmethod
|
||||
def get_constants(cls):
|
||||
return {
|
||||
name: Config.get_constant(name) for name in cls._get_instance().constants
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_constant(cls, name):
|
||||
value = cls._get_instance().constants.get(name)
|
||||
if value is None:
|
||||
return None
|
||||
return value() if callable(value) else value
|
||||
|
||||
@classmethod
|
||||
def get_cronjobs(cls):
|
||||
return cls._get_instance().cronjobs
|
||||
|
||||
@classmethod
|
||||
def _get_default_cfgfile(cls) -> Optional[str]:
|
||||
for location in cls._cfgfile_locations:
|
||||
if os.path.isfile(location):
|
||||
return location
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def init(cfgfile=None):
|
||||
@classmethod
|
||||
def init(cls, cfgfile: Optional[str] = None):
|
||||
"""
|
||||
Initializes the config object singleton
|
||||
Params:
|
||||
cfgfile -- path to the config file - default: _cfgfile_locations
|
||||
"""
|
||||
global _default_config_instance
|
||||
_default_config_instance = Config(cfgfile)
|
||||
return cls._get_instance(cfgfile, force_reload=True)
|
||||
|
||||
@staticmethod
|
||||
def get(key: Optional[str] = None):
|
||||
@classmethod
|
||||
def get(cls, key: Optional[str] = None):
|
||||
"""
|
||||
Get a config value or the whole configuration object.
|
||||
|
||||
:param key: Configuration entry to get (default: all entries).
|
||||
"""
|
||||
global _default_config_instance
|
||||
if _default_config_instance is None:
|
||||
_default_config_instance = Config()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
config = cls._get_instance()._config.copy()
|
||||
if key:
|
||||
return _default_config_instance._config.get(key)
|
||||
|
||||
return _default_config_instance._config
|
||||
return config.get(key)
|
||||
return config
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -2,6 +2,7 @@ import asyncio
|
|||
import importlib
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from threading import RLock
|
||||
from typing import Optional, Any
|
||||
|
||||
|
@ -11,36 +12,62 @@ from ..utils import get_enabled_plugins
|
|||
|
||||
logger = logging.getLogger('platypush:context')
|
||||
|
||||
# Map: backend_name -> backend_instance
|
||||
backends = {}
|
||||
|
||||
# Map: plugin_name -> plugin_instance
|
||||
plugins = {}
|
||||
@dataclass
|
||||
class Context:
|
||||
"""
|
||||
Data class to hold the context of the application.
|
||||
"""
|
||||
|
||||
# backend_name -> backend_instance
|
||||
backends: dict = field(default_factory=dict)
|
||||
# plugin_name -> plugin_instance
|
||||
plugins: dict = field(default_factory=dict)
|
||||
# Reference to the main application bus
|
||||
bus: Optional[Bus] = None
|
||||
|
||||
|
||||
_ctx = Context()
|
||||
|
||||
# # Map: backend_name -> backend_instance
|
||||
# backends = {}
|
||||
|
||||
# # Map: plugin_name -> plugin_instance
|
||||
# plugins = {}
|
||||
|
||||
# Map: plugin_name -> init_lock to make sure that a plugin isn't initialized
|
||||
# multiple times
|
||||
plugins_init_locks = {}
|
||||
|
||||
# Reference to the main application bus
|
||||
main_bus = None
|
||||
# main_bus = None
|
||||
|
||||
|
||||
def get_context() -> Context:
|
||||
"""
|
||||
Get the current application context.
|
||||
"""
|
||||
|
||||
return _ctx
|
||||
|
||||
|
||||
def register_backends(bus=None, global_scope=False, **kwargs):
|
||||
"""Initialize the backend objects based on the configuration and returns
|
||||
a name -> backend_instance map.
|
||||
"""
|
||||
Initialize the backend objects based on the configuration and returns a
|
||||
name -> backend_instance map.
|
||||
|
||||
Params:
|
||||
bus -- If specific (it usually should), the messages processed by the
|
||||
backends will be posted on this bus.
|
||||
|
||||
kwargs -- Any additional key-value parameters required to initialize the backends
|
||||
kwargs -- Any additional key-value parameters required to initialize
|
||||
the backends
|
||||
"""
|
||||
|
||||
global main_bus
|
||||
if bus:
|
||||
main_bus = bus
|
||||
_ctx.bus = bus
|
||||
|
||||
if global_scope:
|
||||
global backends
|
||||
backends = _ctx.backends
|
||||
else:
|
||||
backends = {}
|
||||
|
||||
|
@ -57,13 +84,16 @@ def register_backends(bus=None, global_scope=False, **kwargs):
|
|||
b = getattr(module, cls_name)(bus=bus, **cfg, **kwargs)
|
||||
backends[name] = b
|
||||
except AttributeError as e:
|
||||
logger.warning('No such class in {}: {}'.format(module.__name__, cls_name))
|
||||
raise RuntimeError(e)
|
||||
logger.warning('No such class in %s: %s', module.__name__, cls_name)
|
||||
raise RuntimeError(e) from e
|
||||
|
||||
return backends
|
||||
|
||||
|
||||
def register_plugins(bus=None):
|
||||
"""
|
||||
Register and start all the ``RunnablePlugin`` configured implementations.
|
||||
"""
|
||||
from ..plugins import RunnablePlugin
|
||||
|
||||
for plugin in get_enabled_plugins().values():
|
||||
|
@ -75,27 +105,25 @@ def register_plugins(bus=None):
|
|||
def get_backend(name):
|
||||
"""Returns the backend instance identified by name if it exists"""
|
||||
|
||||
global backends
|
||||
return backends.get(name)
|
||||
return _ctx.backends.get(name)
|
||||
|
||||
|
||||
def get_plugin(plugin_name, reload=False):
|
||||
"""Registers a plugin instance by name if not registered already, or
|
||||
returns the registered plugin instance"""
|
||||
global plugins
|
||||
global plugins_init_locks
|
||||
|
||||
"""
|
||||
Registers a plugin instance by name if not registered already, or returns
|
||||
the registered plugin instance.
|
||||
"""
|
||||
if plugin_name not in plugins_init_locks:
|
||||
plugins_init_locks[plugin_name] = RLock()
|
||||
|
||||
if plugin_name in plugins and not reload:
|
||||
return plugins[plugin_name]
|
||||
if plugin_name in _ctx.plugins and not reload:
|
||||
return _ctx.plugins[plugin_name]
|
||||
|
||||
try:
|
||||
plugin = importlib.import_module('platypush.plugins.' + plugin_name)
|
||||
except ImportError as e:
|
||||
logger.warning('No such plugin: {}'.format(plugin_name))
|
||||
raise RuntimeError(e)
|
||||
logger.warning('No such plugin: %s', plugin_name)
|
||||
raise RuntimeError(e) from e
|
||||
|
||||
# e.g. plugins.music.mpd main class: MusicMpdPlugin
|
||||
cls_name = ''
|
||||
|
@ -120,30 +148,34 @@ def get_plugin(plugin_name, reload=False):
|
|||
try:
|
||||
plugin_class = getattr(plugin, cls_name)
|
||||
except AttributeError as e:
|
||||
logger.warning(
|
||||
'No such class in {}: {} [error: {}]'.format(plugin_name, cls_name, str(e))
|
||||
)
|
||||
raise RuntimeError(e)
|
||||
logger.warning('No such class in %s: %s [error: %s]', plugin_name, cls_name, e)
|
||||
raise RuntimeError(e) from e
|
||||
|
||||
with plugins_init_locks[plugin_name]:
|
||||
if plugins.get(plugin_name) and not reload:
|
||||
return plugins[plugin_name]
|
||||
plugins[plugin_name] = plugin_class(**plugin_conf)
|
||||
if _ctx.plugins.get(plugin_name) and not reload:
|
||||
return _ctx.plugins[plugin_name]
|
||||
_ctx.plugins[plugin_name] = plugin_class(**plugin_conf)
|
||||
|
||||
return plugins[plugin_name]
|
||||
return _ctx.plugins[plugin_name]
|
||||
|
||||
|
||||
def get_bus() -> Bus:
|
||||
global main_bus
|
||||
if main_bus:
|
||||
return main_bus
|
||||
|
||||
"""
|
||||
Get or register the main application bus.
|
||||
"""
|
||||
from platypush.bus.redis import RedisBus
|
||||
|
||||
return RedisBus()
|
||||
if _ctx.bus:
|
||||
return _ctx.bus
|
||||
|
||||
_ctx.bus = RedisBus()
|
||||
return _ctx.bus
|
||||
|
||||
|
||||
def get_or_create_event_loop():
|
||||
def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
|
||||
"""
|
||||
Get or create a new event loop
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except (DeprecationWarning, RuntimeError):
|
||||
|
|
|
@ -35,7 +35,7 @@ class MultiLevelSwitchEntityManager(EntityManager, ABC):
|
|||
|
||||
@abstractmethod
|
||||
def set_value( # pylint: disable=redefined-builtin
|
||||
self, *entities, property=None, value=None, **__
|
||||
self, *entities, property=None, data=None, **__
|
||||
):
|
||||
"""Set a value"""
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -5,7 +5,7 @@ import time
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import wraps
|
||||
from typing import Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from platypush.bus import Bus
|
||||
from platypush.common import ExtensionWithManifest
|
||||
|
@ -13,12 +13,20 @@ from platypush.event import EventGenerator
|
|||
from platypush.message.response import Response
|
||||
from platypush.utils import get_decorators, get_plugin_name_by_class, set_thread_name
|
||||
|
||||
stop_timeout = 5 # Plugin stop timeout in seconds
|
||||
PLUGIN_STOP_TIMEOUT = 5 # Plugin stop timeout in seconds
|
||||
|
||||
|
||||
def action(f):
|
||||
def action(f: Callable[..., Any]) -> Callable[..., Response]:
|
||||
"""
|
||||
Decorator used to wrap the methods in the plugin classes that should be
|
||||
exposed as actions.
|
||||
|
||||
It wraps the method's response into a generic
|
||||
:meth:`platypush.message.response.Response` object.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def _execute_action(*args, **kwargs):
|
||||
def _execute_action(*args, **kwargs) -> Response:
|
||||
response = Response()
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
|
@ -61,7 +69,7 @@ class Plugin(EventGenerator, ExtensionWithManifest): # lgtm [py/missing-call-to
|
|||
def run(self, method, *args, **kwargs):
|
||||
assert (
|
||||
method in self.registered_actions
|
||||
), '{} is not a registered action on {}'.format(method, self.__class__.__name__)
|
||||
), f'{method} is not a registered action on {self.__class__.__name__}'
|
||||
return getattr(self, method)(*args, **kwargs)
|
||||
|
||||
|
||||
|
@ -72,13 +80,13 @@ class RunnablePlugin(Plugin):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
poll_interval: Optional[float] = None,
|
||||
stop_timeout: Optional[float] = stop_timeout,
|
||||
poll_interval: Optional[float] = 30,
|
||||
stop_timeout: Optional[float] = PLUGIN_STOP_TIMEOUT,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
:param poll_interval: How often the :meth:`.loop` function should be
|
||||
execute (default: None, no pause/interval).
|
||||
execute (default: 30 seconds).
|
||||
:param stop_timeout: How long we should wait for any running
|
||||
threads/processes to stop before exiting (default: 5 seconds).
|
||||
"""
|
||||
|
@ -106,26 +114,26 @@ class RunnablePlugin(Plugin):
|
|||
def stop(self):
|
||||
self._should_stop.set()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self.logger.info(f'Waiting for {self.__class__.__name__} to stop')
|
||||
self.logger.info('Waiting for %s to stop', self.__class__.__name__)
|
||||
try:
|
||||
if self._thread:
|
||||
self._thread.join(timeout=self._stop_timeout)
|
||||
if self._thread and self._thread.is_alive():
|
||||
self.logger.warning(
|
||||
f'Timeout (seconds={self._stop_timeout}) on '
|
||||
'exit for the plugin '
|
||||
+ (
|
||||
'Timeout (seconds={%s}) on exit for the plugin %s',
|
||||
self._stop_timeout,
|
||||
(
|
||||
get_plugin_name_by_class(self.__class__)
|
||||
or self.__class__.__name__
|
||||
)
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.warning(f'Could not join thread on stop: {e}')
|
||||
self.logger.warning('Could not join thread on stop: %s', e)
|
||||
|
||||
self.logger.info(f'{self.__class__.__name__} stopped')
|
||||
self.logger.info('%s stopped', self.__class__.__name__)
|
||||
|
||||
def _runner(self):
|
||||
self.logger.info(f'Starting {self.__class__.__name__}')
|
||||
self.logger.info('Starting %s', self.__class__.__name__)
|
||||
|
||||
while not self.should_stop():
|
||||
try:
|
||||
|
|
|
@ -71,7 +71,7 @@ class RedisPlugin(Plugin):
|
|||
try:
|
||||
return self._get_redis().mset(**kwargs)
|
||||
except TypeError:
|
||||
# XXX commit https://github.com/andymccurdy/redis-py/commit/90a52dd5de111f0053bb3ebaa7c78f73a82a1e3e
|
||||
# Commit https://github.com/andymccurdy/redis-py/commit/90a52dd5de111f0053bb3ebaa7c78f73a82a1e3e
|
||||
# broke back-compatibility with the previous way of passing
|
||||
# key-value pairs to mset directly on kwargs. This try-catch block
|
||||
# is to support things on all the redis-py versions
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import contextlib
|
||||
import ipaddress
|
||||
from typing import List, Optional
|
||||
from typing import Collection, Dict, List, Mapping, Optional, Union
|
||||
|
||||
from platypush.plugins import action
|
||||
from platypush.plugins.switch import SwitchPlugin
|
||||
from platypush.entities import Entity, SwitchEntityManager
|
||||
from platypush.entities.switches import Switch
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.utils.workers import Workers
|
||||
|
||||
from .lib import WemoRunner
|
||||
from .scanner import Scanner
|
||||
|
||||
|
||||
class SwitchWemoPlugin(SwitchPlugin):
|
||||
class SwitchWemoPlugin(RunnablePlugin, SwitchEntityManager):
|
||||
"""
|
||||
Plugin to control a Belkin WeMo smart switches
|
||||
(https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
|
||||
|
@ -20,27 +21,46 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
devices=None,
|
||||
devices: Optional[Union[Collection[str], Mapping[str, str]]] = None,
|
||||
netmask: Optional[str] = None,
|
||||
port: int = _default_port,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
:param devices: List of IP addresses or name->address map containing the WeMo Switch devices to control.
|
||||
This plugin previously used ouimeaux for auto-discovery but it's been dropped because
|
||||
1. too slow 2. too heavy 3. auto-discovery failed too often.
|
||||
This plugin previously used ``ouimeaux`` for auto-discovery, but it's
|
||||
been dropped because:
|
||||
|
||||
1. Too slow
|
||||
2. Too heavy
|
||||
3. Auto-discovery failed too often
|
||||
|
||||
However, this also means that you now have to specify either:
|
||||
|
||||
- ``devices``: The devices you want to control, as a static list/map
|
||||
- ``netmask``: The IP netmask that should be scanned for WeMo devices
|
||||
|
||||
:param devices: List of IP addresses or name->address map containing
|
||||
the WeMo Switch devices to control.
|
||||
:type devices: list or dict
|
||||
|
||||
:param netmask: Alternatively to a list of static IP->name pairs, you can specify the network mask where
|
||||
the devices should be scanned (e.g. '192.168.1.0/24')
|
||||
:param netmask: Alternatively to a list of static IP->name pairs, you
|
||||
can specify the network mask where the devices should be scanned
|
||||
(e.g. '192.168.1.0/24')
|
||||
|
||||
:param port: Port where the WeMo devices are expected to expose the RPC/XML over HTTP service (default: 49153)
|
||||
:param port: Port where the WeMo devices are expected to expose the
|
||||
RPC/XML over HTTP service (default: 49153)
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
assert devices or netmask, (
|
||||
'Please specify either a static list of devices (either a list of '
|
||||
'IP addresses or a name->address map) or an IP netmask to scan for '
|
||||
'devices'
|
||||
)
|
||||
|
||||
self.port = port
|
||||
self.netmask = netmask
|
||||
self._devices = {}
|
||||
self._devices: Dict[str, str] = {}
|
||||
self._init_devices(devices)
|
||||
|
||||
def _init_devices(self, devices):
|
||||
|
@ -55,34 +75,6 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
|
||||
self._addresses = set(self._devices.values())
|
||||
|
||||
@property
|
||||
def switches(self) -> List[dict]:
|
||||
"""
|
||||
Get the list of available devices
|
||||
:returns: The list of devices.
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"ip": "192.168.1.123",
|
||||
"name": "Switch 1",
|
||||
"on": true
|
||||
},
|
||||
{
|
||||
"ip": "192.168.1.124",
|
||||
"name": "Switch 2",
|
||||
"on": false
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
return [
|
||||
self.status(device).output # type: ignore
|
||||
for device in self._devices.values()
|
||||
]
|
||||
|
||||
def _get_address(self, device: str) -> str:
|
||||
if device not in self._addresses:
|
||||
with contextlib.suppress(KeyError):
|
||||
|
@ -91,8 +83,20 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
return device
|
||||
|
||||
@action
|
||||
def status(self, device: Optional[str] = None, *_, **__):
|
||||
devices = {device: device} if device else self._devices.copy()
|
||||
# pylint: disable=arguments-differ
|
||||
def status(
|
||||
self,
|
||||
device: Optional[Union[str, Collection[str]]] = None,
|
||||
publish_entities: bool = True,
|
||||
**__
|
||||
) -> List[dict]:
|
||||
if device:
|
||||
if isinstance(device, str):
|
||||
devices = {device: device}
|
||||
else:
|
||||
devices = {d: d for d in device}
|
||||
else:
|
||||
devices = self._devices.copy()
|
||||
|
||||
ret = [
|
||||
{
|
||||
|
@ -104,28 +108,25 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
for (name, addr) in devices.items()
|
||||
]
|
||||
|
||||
self.publish_entities(ret) # type: ignore
|
||||
return ret[0] if device else ret
|
||||
if publish_entities:
|
||||
self.publish_entities(ret)
|
||||
return ret
|
||||
|
||||
def transform_entities(self, devices: List[dict]):
|
||||
from platypush.entities.switches import Switch
|
||||
|
||||
return super().transform_entities( # type: ignore
|
||||
[
|
||||
Switch(
|
||||
id=dev["id"],
|
||||
name=dev["name"],
|
||||
state=dev["on"],
|
||||
data={
|
||||
"ip": dev["ip"],
|
||||
},
|
||||
)
|
||||
for dev in (devices or [])
|
||||
]
|
||||
)
|
||||
def transform_entities(self, entities: Collection[dict]) -> List[Entity]:
|
||||
return [
|
||||
Switch(
|
||||
id=dev["id"],
|
||||
name=dev["name"],
|
||||
state=dev["on"],
|
||||
data={
|
||||
"ip": dev["ip"],
|
||||
},
|
||||
)
|
||||
for dev in (entities or [])
|
||||
]
|
||||
|
||||
@action
|
||||
def on(self, device: str, **_):
|
||||
def on(self, device: str, **_): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Turn a switch on
|
||||
|
||||
|
@ -136,7 +137,7 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
return self.status(device)
|
||||
|
||||
@action
|
||||
def off(self, device: str, **_):
|
||||
def off(self, device: str, **_): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Turn a switch off
|
||||
|
||||
|
@ -147,7 +148,7 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
return self.status(device)
|
||||
|
||||
@action
|
||||
def toggle(self, device: str, *_, **__):
|
||||
def toggle(self, device: str, *_, **__): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Toggle a device on/off state
|
||||
|
||||
|
@ -178,7 +179,9 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
return WemoRunner.get_name(device)
|
||||
|
||||
@action
|
||||
def scan(self, netmask: Optional[str] = None):
|
||||
def scan(
|
||||
self, netmask: Optional[str] = None, publish_entities: bool = True
|
||||
) -> List[dict]:
|
||||
netmask = netmask or self.netmask
|
||||
assert netmask, "Scan not supported: No netmask specified"
|
||||
|
||||
|
@ -190,7 +193,33 @@ class SwitchWemoPlugin(SwitchPlugin):
|
|||
devices = {dev.name: dev.addr for dev in workers.responses}
|
||||
|
||||
self._init_devices(devices)
|
||||
return self.status()
|
||||
return self.status(publish_entities=publish_entities).output
|
||||
|
||||
def main(self):
|
||||
def scan():
|
||||
status = (
|
||||
self.scan(publish_entities=False).output
|
||||
if not self._devices
|
||||
else self.status(self._devices.values(), publish_entities=False).output
|
||||
)
|
||||
|
||||
return {dev['ip']: dev for dev in status}
|
||||
|
||||
devices = {}
|
||||
|
||||
while not self.should_stop():
|
||||
new_devices = scan()
|
||||
updated_devices = {
|
||||
ip: new_devices[ip]
|
||||
for ip, dev in new_devices.items()
|
||||
if any(v != devices.get(ip, {}).get(k) for k, v in dev.items())
|
||||
}
|
||||
|
||||
if updated_devices:
|
||||
self.publish_entities(updated_devices.values())
|
||||
|
||||
devices = new_devices
|
||||
self.wait_stop(self.poll_interval)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -1,35 +1,48 @@
|
|||
import socket
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from platypush.utils.workers import Worker
|
||||
from .lib import WemoRunner
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanResult:
|
||||
def __init__(self, addr: str, name: str, on: bool):
|
||||
self.addr = addr
|
||||
self.name = name
|
||||
self.on = on
|
||||
"""
|
||||
Models a scan result.
|
||||
"""
|
||||
|
||||
addr: str
|
||||
name: str
|
||||
on: bool
|
||||
|
||||
|
||||
class Scanner(Worker):
|
||||
"""
|
||||
Worker class used to scan WeMo devices on the network.
|
||||
"""
|
||||
|
||||
timeout = 1.5
|
||||
|
||||
def __init__(self, port: int = WemoRunner.default_port, *args, **kwargs):
|
||||
def __init__(self, *args, port: int = WemoRunner.default_port, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.port = port
|
||||
|
||||
def process(self, addr: str) -> Optional[ScanResult]:
|
||||
def process(self, msg: str) -> Optional[ScanResult]:
|
||||
addr = msg
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect((addr, self.port))
|
||||
sock.close()
|
||||
|
||||
return ScanResult(addr=addr, name=WemoRunner.get_name(addr), on=WemoRunner.get_state(addr))
|
||||
return ScanResult(
|
||||
addr=addr, name=WemoRunner.get_name(addr), on=WemoRunner.get_state(addr)
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -1,38 +1,71 @@
|
|||
import queue
|
||||
import requests
|
||||
import threading
|
||||
from typing import List, Optional, Union
|
||||
from typing import Any, Collection, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from platypush.plugins import action
|
||||
from platypush.plugins.switch import SwitchPlugin
|
||||
import requests
|
||||
|
||||
from platypush.entities import (
|
||||
DimmerEntityManager,
|
||||
EnumSwitchEntityManager,
|
||||
Entity,
|
||||
LightEntityManager,
|
||||
SwitchEntityManager,
|
||||
)
|
||||
from platypush.entities.devices import Device
|
||||
from platypush.entities.dimmers import Dimmer
|
||||
from platypush.entities.electricity import CurrentSensor, PowerSensor, VoltageSensor
|
||||
from platypush.entities.lights import Light
|
||||
from platypush.entities.humidity import HumiditySensor
|
||||
from platypush.entities.motion import MotionSensor
|
||||
from platypush.entities.sensors import BinarySensor, EnumSensor, NumericSensor
|
||||
from platypush.entities.switches import EnumSwitch, Switch
|
||||
from platypush.entities.temperature import TemperatureSensor
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.schemas.switchbot import DeviceSchema, DeviceStatusSchema, SceneSchema
|
||||
|
||||
from ._constants import DeviceType
|
||||
from ._setters import entity_setters
|
||||
|
||||
class SwitchbotPlugin(SwitchPlugin):
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class SwitchbotPlugin(
|
||||
RunnablePlugin,
|
||||
DimmerEntityManager,
|
||||
EnumSwitchEntityManager,
|
||||
LightEntityManager,
|
||||
SwitchEntityManager,
|
||||
):
|
||||
"""
|
||||
Plugin to interact with the devices registered to a Switchbot (https://www.switch-bot.com/) account/hub.
|
||||
Plugin to interact with the devices registered to a Switchbot
|
||||
(https://www.switch-bot.com/) account/hub.
|
||||
|
||||
The difference between this plugin and :class:`platypush.plugins.switchbot.bluetooth.SwitchbotBluetoothPlugin` is
|
||||
that the latter acts like a Bluetooth hub/bridge that interacts directly with your Switchbot devices, while this
|
||||
plugin requires the devices to be connected to a Switchbot Hub and it controls them through your cloud account.
|
||||
The difference between this plugin and
|
||||
:class:`platypush.plugins.switchbot.bluetooth.SwitchbotBluetoothPlugin` is
|
||||
that the latter acts like a Bluetooth hub/bridge that interacts directly
|
||||
with your Switchbot devices, while this plugin requires the devices to be
|
||||
connected to a Switchbot Hub and it controls them through your cloud
|
||||
account.
|
||||
|
||||
In order to use this plugin:
|
||||
|
||||
- Set up a Switchbot Hub and configure your devices through the Switchbot app.
|
||||
- Follow the steps on the `Switchbot API repo <https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_
|
||||
to get an API token from the app.
|
||||
- Set up a Switchbot Hub and configure your devices through the
|
||||
Switchbot app.
|
||||
- Follow the steps on the `Switchbot API repo
|
||||
<https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_ to
|
||||
get an API token from the app.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, api_token: str, **kwargs):
|
||||
"""
|
||||
:param api_token: API token (see
|
||||
`Getting started with the Switchbot API <https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_).
|
||||
`Getting started with the Switchbot API
|
||||
<https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_).
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._api_token = api_token
|
||||
self._devices_by_id = {}
|
||||
self._devices_by_name = {}
|
||||
self._devices_by_id: Dict[str, dict] = {}
|
||||
self._devices_by_name: Dict[str, dict] = {}
|
||||
|
||||
@staticmethod
|
||||
def _url_for(*args, device=None):
|
||||
|
@ -42,6 +75,7 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
url += '/'.join(args)
|
||||
return url
|
||||
|
||||
# pylint: disable=keyword-arg-before-vararg
|
||||
def _run(self, method: str = 'get', *args, device=None, **kwargs):
|
||||
response = getattr(requests, method)(
|
||||
self._url_for(*args, device=device),
|
||||
|
@ -50,6 +84,7 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
timeout=10,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
@ -61,15 +96,22 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
return response.get('body')
|
||||
|
||||
def _get_device(self, device: str, use_cache=True):
|
||||
@staticmethod
|
||||
def _split_device_id_and_property(device: str) -> Tuple[str, Optional[str]]:
|
||||
tokens = device.split(':')[:2]
|
||||
return tokens[0], (tokens[1] if len(tokens) == 2 else None)
|
||||
|
||||
def _get_device(self, device: str, use_cache=True) -> dict:
|
||||
if not use_cache:
|
||||
self.devices()
|
||||
|
||||
if device in self._devices_by_id:
|
||||
return self._devices_by_id[device]
|
||||
if device in self._devices_by_name:
|
||||
return self._devices_by_name[device]
|
||||
|
||||
device, _ = self._split_device_id_and_property(device)
|
||||
if device in self._devices_by_id:
|
||||
return self._devices_by_id[device]
|
||||
|
||||
assert use_cache, f'Device not found: {device}'
|
||||
return self._get_device(device, use_cache=False)
|
||||
|
||||
|
@ -105,28 +147,363 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
return devices
|
||||
|
||||
def transform_entities(self, devices: List[dict]):
|
||||
from platypush.entities.switches import Switch
|
||||
@staticmethod
|
||||
def _get_device_metadata(device: dict) -> dict:
|
||||
return {
|
||||
"device_type": device.get("device_type"),
|
||||
"is_virtual": device.get("is_virtual", False),
|
||||
"hub_id": device.get("hub_id"),
|
||||
}
|
||||
|
||||
return super().transform_entities( # type: ignore
|
||||
[
|
||||
Switch(
|
||||
id=dev["id"],
|
||||
name=dev["name"],
|
||||
state=dev.get("on"),
|
||||
is_write_only=True,
|
||||
data={
|
||||
"device_type": dev.get("device_type"),
|
||||
"is_virtual": dev.get("is_virtual", False),
|
||||
"hub_id": dev.get("hub_id"),
|
||||
},
|
||||
)
|
||||
for dev in (devices or [])
|
||||
if dev.get('device_type') == 'Bot'
|
||||
]
|
||||
@classmethod
|
||||
def _get_device_base(cls, device_dict: dict) -> Device:
|
||||
args: Dict[str, Any] = {
|
||||
'data': cls._get_device_metadata(device_dict),
|
||||
}
|
||||
|
||||
return Device(
|
||||
id=f'{device_dict["id"]}',
|
||||
name=f'{device_dict["name"]}',
|
||||
**args,
|
||||
)
|
||||
|
||||
def _worker(
|
||||
@staticmethod
|
||||
def _matches_device_types(device: dict, *device_types: DeviceType) -> bool:
|
||||
return device.get('device_type') in {
|
||||
device_type.value for device_type in device_types
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_bots(cls, *entities: dict) -> List[EnumSwitch]:
|
||||
return [
|
||||
EnumSwitch(
|
||||
id=dev["id"],
|
||||
name=dev["name"],
|
||||
value="on" if dev.get("on") else "off",
|
||||
values=["on", "off", "press"],
|
||||
is_write_only=True,
|
||||
data=cls._get_device_metadata(dev),
|
||||
)
|
||||
for dev in (entities or [])
|
||||
if cls._matches_device_types(dev, DeviceType.BOT)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _get_lights(cls, *entities: dict) -> List[Light]:
|
||||
return [
|
||||
Light(
|
||||
id=dev["id"],
|
||||
name=dev["name"],
|
||||
on="on" if dev.get("on") else "off",
|
||||
brightness=dev.get("brightness"),
|
||||
color_temperature=dev.get("color_temperature"),
|
||||
color=dev.get("color"),
|
||||
data=cls._get_device_metadata(dev),
|
||||
)
|
||||
for dev in (entities or [])
|
||||
if cls._matches_device_types(
|
||||
dev,
|
||||
DeviceType.CEILING_LIGHT,
|
||||
DeviceType.CEILING_LIGHT_PRO,
|
||||
DeviceType.COLOR_BULB,
|
||||
DeviceType.STRIP_LIGHT,
|
||||
)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _get_curtains(cls, *entities: dict) -> List[Dimmer]:
|
||||
return [
|
||||
Dimmer(
|
||||
id=dev["id"],
|
||||
name=dev["name"],
|
||||
value=dev.get("position"),
|
||||
min=0,
|
||||
max=100,
|
||||
unit='%',
|
||||
data=cls._get_device_metadata(dev),
|
||||
)
|
||||
for dev in (entities or [])
|
||||
if cls._matches_device_types(dev, DeviceType.CURTAIN)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _get_meters(cls, device_dict: dict) -> List[Device]:
|
||||
devices = [cls._get_device_base(device_dict)]
|
||||
if device_dict.get('temperature') is not None:
|
||||
devices[0].children.append(
|
||||
TemperatureSensor(
|
||||
id=f'{device_dict["id"]}:temperature',
|
||||
name='Temperature',
|
||||
value=device_dict['temperature'],
|
||||
unit='C',
|
||||
)
|
||||
)
|
||||
|
||||
if device_dict.get('humidity') is not None:
|
||||
devices[0].children.append(
|
||||
HumiditySensor(
|
||||
id=f'{device_dict["id"]}:humidity',
|
||||
name='Humidity',
|
||||
value=device_dict['humidity'],
|
||||
min=0,
|
||||
max=100,
|
||||
unit='%',
|
||||
)
|
||||
)
|
||||
|
||||
if not devices[0].children:
|
||||
return []
|
||||
return devices
|
||||
|
||||
@classmethod
|
||||
def _get_motion_sensors(cls, device_dict: dict) -> List[Device]:
|
||||
devices = [cls._get_device_base(device_dict)]
|
||||
if device_dict.get('moveDetected') is not None:
|
||||
devices[0].children.append(
|
||||
MotionSensor(
|
||||
id=f'{device_dict["id"]}:motion',
|
||||
name='Motion Detected',
|
||||
value=bool(device_dict['moveDetected']),
|
||||
)
|
||||
)
|
||||
|
||||
if device_dict.get('brightness') is not None:
|
||||
devices[0].children.append(
|
||||
BinarySensor(
|
||||
id=f'{device_dict["id"]}:brightness',
|
||||
name='Bright',
|
||||
value=device_dict['brightness'] == 'bright',
|
||||
)
|
||||
)
|
||||
|
||||
if not devices[0].children:
|
||||
return []
|
||||
return devices
|
||||
|
||||
@classmethod
|
||||
def _get_contact_sensors(cls, device_dict: dict) -> List[Device]:
|
||||
devices = cls._get_motion_sensors(device_dict)
|
||||
if not devices:
|
||||
return []
|
||||
|
||||
if device_dict.get('openState') is not None:
|
||||
devices[0].children.append(
|
||||
EnumSensor(
|
||||
id=f'{device_dict["id"]}:open',
|
||||
name='Open State',
|
||||
value=device_dict['openState'],
|
||||
values=['open', 'close', 'timeOutNotClose'],
|
||||
)
|
||||
)
|
||||
|
||||
return devices
|
||||
|
||||
@classmethod
|
||||
def _get_sensors(cls, *entities: dict) -> List[Device]:
|
||||
sensors: List[Entity] = []
|
||||
for dev in entities:
|
||||
if cls._matches_device_types(dev, DeviceType.METER, DeviceType.METER_PLUS):
|
||||
sensors.extend(cls._get_meters(dev))
|
||||
elif cls._matches_device_types(dev, DeviceType.MOTION_SENSOR):
|
||||
sensors.extend(cls._get_motion_sensors(dev))
|
||||
elif cls._matches_device_types(dev, DeviceType.CONTACT_SENSOR):
|
||||
sensors.extend(cls._get_contact_sensors(dev))
|
||||
|
||||
return sensors
|
||||
|
||||
@classmethod
|
||||
def _get_humidifiers(cls, *entities: dict) -> List[Device]:
|
||||
humidifiers = [
|
||||
dev
|
||||
for dev in entities
|
||||
if cls._matches_device_types(dev, DeviceType.HUMIDIFIER)
|
||||
]
|
||||
|
||||
devs = [Device(**cls._get_device_base(dev)) for dev in humidifiers]
|
||||
|
||||
for dev_dict, entity in zip(humidifiers, devs):
|
||||
if dev_dict.get('power') is not None:
|
||||
entity.children.append(
|
||||
Switch(
|
||||
id=f'{dev_dict["id"]}:state',
|
||||
name='State',
|
||||
state=cls._is_on(dev_dict['power']),
|
||||
)
|
||||
)
|
||||
|
||||
if dev_dict.get('auto') is not None:
|
||||
entity.children.append(
|
||||
Switch(
|
||||
id=f'{dev_dict["id"]}:auto',
|
||||
name='Automatic Mode',
|
||||
state=cls._is_on(dev_dict['auto']),
|
||||
)
|
||||
)
|
||||
|
||||
if dev_dict.get('child_lock') is not None:
|
||||
entity.children.append(
|
||||
Switch(
|
||||
id=f'{dev_dict["id"]}:child_lock',
|
||||
name='Child Lock',
|
||||
state=cls._is_on(dev_dict['child_lock']),
|
||||
)
|
||||
)
|
||||
|
||||
if dev_dict.get('nebulization_efficiency') is not None:
|
||||
entity.children.append(
|
||||
Dimmer(
|
||||
id=f'{dev_dict["id"]}:nebulization_efficiency',
|
||||
name='Nebulization Efficiency',
|
||||
value=cls._is_on(dev_dict['nebulization_efficiency']),
|
||||
min=0,
|
||||
max=100,
|
||||
)
|
||||
)
|
||||
|
||||
if dev_dict.get('low_water') is not None:
|
||||
entity.children.append(
|
||||
BinarySensor(
|
||||
id=f'{dev_dict["id"]}:low_water',
|
||||
name='Low Water',
|
||||
value=cls._is_on(dev_dict['low_water']),
|
||||
)
|
||||
)
|
||||
|
||||
if dev_dict.get('temperature') is not None:
|
||||
entity.children.append(
|
||||
TemperatureSensor(
|
||||
id=f'{dev_dict["id"]}:temperature',
|
||||
name='temperature',
|
||||
value=dev_dict['temperature'],
|
||||
)
|
||||
)
|
||||
|
||||
if dev_dict.get('humidity') is not None:
|
||||
entity.children.append(
|
||||
HumiditySensor(
|
||||
id=f'{dev_dict["id"]}:humidity',
|
||||
name='humidity',
|
||||
value=dev_dict['humidity'],
|
||||
)
|
||||
)
|
||||
|
||||
return devs
|
||||
|
||||
@classmethod
|
||||
def _get_locks(cls, *entities: dict) -> List[Device]:
|
||||
locks = [
|
||||
dev
|
||||
for dev in (entities or [])
|
||||
if cls._matches_device_types(dev, DeviceType.LOCK)
|
||||
]
|
||||
|
||||
devices = [Device(**cls._get_device_base(plug)) for plug in locks]
|
||||
|
||||
for plug, device in zip(locks, devices):
|
||||
if plug.get('locked') is not None:
|
||||
device.children.append(
|
||||
Switch(
|
||||
id=f'{plug["id"]}:locked',
|
||||
name='Locked',
|
||||
state=cls._is_on(plug['locked']),
|
||||
)
|
||||
)
|
||||
|
||||
if plug.get('door_open') is not None:
|
||||
device.children.append(
|
||||
BinarySensor(
|
||||
id=f'{plug["id"]}:door_open',
|
||||
name='Door Open',
|
||||
value=cls._is_on(plug['door_open']),
|
||||
)
|
||||
)
|
||||
|
||||
return devices
|
||||
|
||||
@classmethod
|
||||
def _get_plugs(cls, *entities: dict) -> List[Device]:
|
||||
plugs = [
|
||||
dev
|
||||
for dev in (entities or [])
|
||||
if cls._matches_device_types(
|
||||
dev, DeviceType.PLUG, DeviceType.PLUG_MINI_JP, DeviceType.PLUG_MINI_US
|
||||
)
|
||||
]
|
||||
|
||||
devices = [Device(**cls._get_device_base(plug)) for plug in plugs]
|
||||
|
||||
for plug, device in zip(plugs, devices):
|
||||
if plug.get('on') is not None:
|
||||
device.children.append(
|
||||
Switch(
|
||||
id=f'{plug["id"]}:state',
|
||||
name='State',
|
||||
state=cls._is_on(plug['on']),
|
||||
)
|
||||
)
|
||||
|
||||
if plug.get('power') is not None:
|
||||
device.children.append(
|
||||
PowerSensor(
|
||||
id=f'{plug["id"]}:power',
|
||||
name='Power',
|
||||
value=plug['power'],
|
||||
unit='W',
|
||||
)
|
||||
)
|
||||
|
||||
if plug.get('voltage') is not None:
|
||||
device.children.append(
|
||||
VoltageSensor(
|
||||
id=f'{plug["id"]}:voltage',
|
||||
name='Voltage',
|
||||
value=plug['voltage'],
|
||||
unit='V',
|
||||
)
|
||||
)
|
||||
|
||||
if plug.get('current') is not None:
|
||||
device.children.append(
|
||||
CurrentSensor(
|
||||
id=f'{plug["id"]}:current',
|
||||
name='Current',
|
||||
value=plug['current'],
|
||||
unit='A',
|
||||
)
|
||||
)
|
||||
|
||||
if plug.get('active_time') is not None:
|
||||
device.children.append(
|
||||
NumericSensor(
|
||||
id=f'{plug["id"]}:active_time',
|
||||
name='Active Time',
|
||||
value=plug['active_time'],
|
||||
unit='min',
|
||||
)
|
||||
)
|
||||
|
||||
return devices
|
||||
|
||||
@staticmethod
|
||||
def _is_on(state: Union[bool, str, int]) -> bool:
|
||||
if isinstance(state, str):
|
||||
state = state.lower()
|
||||
else:
|
||||
state = bool(state)
|
||||
return state in {'on', 'true', '1', True}
|
||||
|
||||
def transform_entities(self, entities: Collection[dict]) -> Collection[Entity]:
|
||||
return [
|
||||
*self._get_bots(*entities),
|
||||
*self._get_curtains(*entities),
|
||||
*self._get_humidifiers(*entities),
|
||||
*self._get_lights(*entities),
|
||||
*self._get_locks(*entities),
|
||||
*self._get_plugs(*entities),
|
||||
*self._get_sensors(*entities),
|
||||
]
|
||||
|
||||
def _worker( # pylint: disable=keyword-arg-before-vararg
|
||||
self,
|
||||
q: queue.Queue,
|
||||
method: str = 'get',
|
||||
|
@ -153,14 +530,16 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
q.put(e)
|
||||
|
||||
@action
|
||||
def status(self, device: Optional[str] = None) -> Union[dict, List[dict]]:
|
||||
# pylint: disable=arguments-differ
|
||||
def status(
|
||||
self, device: Optional[str] = None, publish_entities: bool = True, **_
|
||||
) -> Union[dict, List[dict]]:
|
||||
"""
|
||||
Get the status of all the registered devices or of a specific device.
|
||||
|
||||
:param device: Filter by device ID or name.
|
||||
:return: .. schema:: switchbot.DeviceStatusSchema(many=True)
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
devices = self.devices().output
|
||||
if device:
|
||||
device_info = self._get_device(device)
|
||||
|
@ -175,7 +554,7 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
}
|
||||
|
||||
devices_by_id = {dev['id']: dev for dev in devices}
|
||||
queues = [queue.Queue()] * len(devices)
|
||||
queues: List[queue.Queue] = [queue.Queue()] * len(devices)
|
||||
workers = [
|
||||
threading.Thread(
|
||||
target=self._worker,
|
||||
|
@ -205,7 +584,8 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
for worker in workers:
|
||||
worker.join()
|
||||
|
||||
self.publish_entities(results) # type: ignore
|
||||
if publish_entities:
|
||||
self.publish_entities(results)
|
||||
return results
|
||||
|
||||
@action
|
||||
|
@ -215,11 +595,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
return self._run('post', 'commands', device=device, json={'command': 'press'})
|
||||
dev = self._get_device(device)
|
||||
return self._run('post', 'commands', device=dev, json={'command': 'press'})
|
||||
|
||||
@action
|
||||
def toggle(self, device: str, **kwargs):
|
||||
def toggle(self, device: str, **_): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Shortcut for :meth:`.press`.
|
||||
|
||||
|
@ -228,29 +608,44 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
return self.press(device)
|
||||
|
||||
@action
|
||||
def on(self, device: str, **kwargs):
|
||||
def on(self, device: str, **_): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Send a turn-on command to a device
|
||||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
return self._run('post', 'commands', device=device, json={'command': 'turnOn'})
|
||||
dev = self._get_device(device)
|
||||
return self._run('post', 'commands', device=dev, json={'command': 'turnOn'})
|
||||
|
||||
@action
|
||||
def off(self, device: str, **kwargs):
|
||||
def off(self, device: str, **_): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Send a turn-off command to a device
|
||||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
return self._run('post', 'commands', device=device, json={'command': 'turnOff'})
|
||||
dev = self._get_device(device)
|
||||
return self._run('post', 'commands', device=dev, json={'command': 'turnOff'})
|
||||
|
||||
@property
|
||||
def switches(self) -> List[dict]:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return [dev for dev in self.status().output if 'on' in dev]
|
||||
@action
|
||||
def lock(self, device: str, **_):
|
||||
"""
|
||||
Lock a compatible lock device.
|
||||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
dev = self._get_device(device)
|
||||
return self._run('post', 'commands', device=dev, json={'command': 'lock'})
|
||||
|
||||
@action
|
||||
def unlock(self, device: str, **_):
|
||||
"""
|
||||
Unlock a compatible lock device.
|
||||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
dev = self._get_device(device)
|
||||
return self._run('post', 'commands', device=dev, json={'command': 'unlock'})
|
||||
|
||||
@action
|
||||
def set_curtain_position(self, device: str, position: int):
|
||||
|
@ -260,11 +655,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
:param device: Device name or ID.
|
||||
:param position: An integer between 0 (open) and 100 (closed).
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'setPosition',
|
||||
'commandType': 'command',
|
||||
|
@ -278,13 +673,17 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
Set the nebulization efficiency of a humidifier device.
|
||||
|
||||
:param device: Device name or ID.
|
||||
:param efficiency: An integer between 0 (open) and 100 (closed) or `auto`.
|
||||
:param efficiency: Possible values:
|
||||
|
||||
- ``auto``: Automatic mode.
|
||||
- A value between ``0`` and ``100``.
|
||||
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'setMode',
|
||||
'commandType': 'command',
|
||||
|
@ -300,7 +699,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
:param device: Device name or ID.
|
||||
:param speed: Speed between 1 and 4.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
status = self.status(device=device).output
|
||||
mode = status.get('mode')
|
||||
swing_range = status.get('swing_range')
|
||||
|
@ -323,7 +721,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
:param device: Device name or ID.
|
||||
:param mode: Fan mode (1 or 2).
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
status = self.status(device=device).output
|
||||
speed = status.get('speed')
|
||||
swing_range = status.get('swing_range')
|
||||
|
@ -346,7 +743,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
:param device: Device name or ID.
|
||||
:param swing_range: Swing range angle, between 0 and 120.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
status = self.status(device=device).output
|
||||
speed = status.get('speed')
|
||||
mode = status.get('mode')
|
||||
|
@ -369,7 +765,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
:param device: Device name or ID.
|
||||
:param temperature: Temperature, in Celsius.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
status = self.status(device=device).output
|
||||
mode = status.get('mode')
|
||||
fan_speed = status.get('fan_speed')
|
||||
|
@ -401,7 +796,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
* 5: ``heat``
|
||||
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
status = self.status(device=device).output
|
||||
temperature = status.get('temperature')
|
||||
fan_speed = status.get('fan_speed')
|
||||
|
@ -432,7 +826,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
* 4: ``high``
|
||||
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
status = self.status(device=device).output
|
||||
temperature = status.get('temperature')
|
||||
mode = status.get('mode')
|
||||
|
@ -457,11 +850,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
:param device: Device name or ID.
|
||||
:param channel: Channel number.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'SetChannel',
|
||||
'commandType': 'command',
|
||||
|
@ -476,11 +869,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'volumeAdd',
|
||||
'commandType': 'command',
|
||||
|
@ -494,11 +887,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'volumeSub',
|
||||
'commandType': 'command',
|
||||
|
@ -512,11 +905,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'setMute',
|
||||
'commandType': 'command',
|
||||
|
@ -530,11 +923,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'channelAdd',
|
||||
'commandType': 'command',
|
||||
|
@ -548,11 +941,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'channelSub',
|
||||
'commandType': 'command',
|
||||
|
@ -566,11 +959,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'Play',
|
||||
'commandType': 'command',
|
||||
|
@ -584,11 +977,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'Pause',
|
||||
'commandType': 'command',
|
||||
|
@ -596,17 +989,17 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
)
|
||||
|
||||
@action
|
||||
def stop(self, device: str):
|
||||
def ir_stop(self, device: str):
|
||||
"""
|
||||
Send stop IR event to a device (for DVD and Speaker).
|
||||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'Stop',
|
||||
'commandType': 'command',
|
||||
|
@ -620,11 +1013,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'FastForward',
|
||||
'commandType': 'command',
|
||||
|
@ -638,11 +1031,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'Rewind',
|
||||
'commandType': 'command',
|
||||
|
@ -656,11 +1049,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'Next',
|
||||
'commandType': 'command',
|
||||
|
@ -674,11 +1067,11 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param device: Device name or ID.
|
||||
"""
|
||||
device = self._get_device(device)
|
||||
dev = self._get_device(device)
|
||||
return self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=device,
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'Previous',
|
||||
'commandType': 'command',
|
||||
|
@ -701,7 +1094,6 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
|
||||
:param scene: Scene ID or name.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences
|
||||
scenes = [
|
||||
s
|
||||
for s in self.scenes().output
|
||||
|
@ -711,5 +1103,119 @@ class SwitchbotPlugin(SwitchPlugin):
|
|||
assert scenes, f'No such scene: {scene}'
|
||||
return self._run('post', 'scenes', scenes[0]['id'], 'execute')
|
||||
|
||||
@action
|
||||
# pylint: disable=redefined-builtin,arguments-differ
|
||||
def set_value(self, device: str, property=None, data=None, **__):
|
||||
entity = self._to_entity(device, property)
|
||||
assert entity, f'No such entity: "{device}"'
|
||||
|
||||
dt = entity.data.get('device_type')
|
||||
assert dt, f'Could not infer the device type for "{device}"'
|
||||
|
||||
device_type = DeviceType(dt)
|
||||
setter_class = entity_setters.get(device_type)
|
||||
assert setter_class, f'No setters found for device type "{device_type}"'
|
||||
|
||||
setter = setter_class(entity)
|
||||
return setter(property=property, value=data)
|
||||
|
||||
def _to_entity(
|
||||
self,
|
||||
device: str,
|
||||
property: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
) -> Optional[Entity]:
|
||||
dev = self._get_device(device)
|
||||
entities = list(self.transform_entities([dev]))
|
||||
if not entities:
|
||||
return None
|
||||
if len(entities) == 1:
|
||||
return entities[0]
|
||||
if not property:
|
||||
device, property = self._split_device_id_and_property(device)
|
||||
assert property, 'No property specified'
|
||||
|
||||
entity_id = f'{device}:{property}'
|
||||
return next(iter([e for e in entities if e.id == entity_id]), None)
|
||||
|
||||
@action
|
||||
def set_lights(
|
||||
self,
|
||||
*_,
|
||||
lights: Collection[str],
|
||||
on: Optional[bool] = None,
|
||||
brightness: Optional[int] = None,
|
||||
hex: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
temperature: Optional[int] = None,
|
||||
**__,
|
||||
):
|
||||
"""
|
||||
Change the settings for compatible lights.
|
||||
|
||||
:param lights: Light names or IDs.
|
||||
:param on: Turn on the lights.
|
||||
:param brightness: Set the brightness of the lights.
|
||||
:param hex: Set the color of the lights.
|
||||
:param temperature: Set the temperature of the lights.
|
||||
"""
|
||||
devices = [self._get_device(light) for light in lights]
|
||||
for dev in devices:
|
||||
if on is not None:
|
||||
method = self.on if on else self.off
|
||||
method(dev['id'])
|
||||
|
||||
if brightness is not None:
|
||||
self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'setBrightness',
|
||||
'commandType': 'command',
|
||||
'parameter': brightness,
|
||||
},
|
||||
)
|
||||
|
||||
if hex is not None:
|
||||
self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'setColor',
|
||||
'commandType': 'command',
|
||||
'parameter': hex,
|
||||
},
|
||||
)
|
||||
|
||||
if temperature is not None:
|
||||
self._run(
|
||||
'post',
|
||||
'commands',
|
||||
device=dev,
|
||||
json={
|
||||
'command': 'setColorTemperature',
|
||||
'commandType': 'command',
|
||||
'parameter': temperature,
|
||||
},
|
||||
)
|
||||
|
||||
def main(self):
|
||||
entities = {}
|
||||
|
||||
while not self.should_stop():
|
||||
status = self.status(publish_entities=False).output
|
||||
new_entities = {e['id']: e for e in status}
|
||||
updated_entities = {
|
||||
id: e
|
||||
for id, e in new_entities.items()
|
||||
if any(v != entities.get(id, {}).get(k) for k, v in e.items())
|
||||
}
|
||||
|
||||
if updated_entities:
|
||||
self.publish_entities(updated_entities.values())
|
||||
|
||||
entities = new_entities
|
||||
self.wait_stop(self.poll_interval)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class DeviceType(Enum):
|
||||
"""
|
||||
Constants used for the `device_type` attribute.
|
||||
|
||||
Reference: https://github.com/OpenWonderLabs/SwitchBotAPI
|
||||
"""
|
||||
|
||||
BLIND_TILT = 'Blind Tilt'
|
||||
BOT = 'Bot'
|
||||
CEILING_LIGHT = 'Ceiling Light'
|
||||
CEILING_LIGHT_PRO = 'Ceiling Light Pro'
|
||||
COLOR_BULB = 'Color Bulb'
|
||||
CONTACT_SENSOR = 'Contact Sensor'
|
||||
CURTAIN = 'Curtain'
|
||||
HUMIDIFIER = 'Humidifier'
|
||||
KEYPAD = 'Keypad'
|
||||
KEYPAD_TOUCH = 'Keypad Touch'
|
||||
LOCK = 'Smart Lock'
|
||||
METER = 'Meter'
|
||||
METER_PLUS = 'Meter Plus'
|
||||
MOTION_SENSOR = 'Motion Sensor'
|
||||
PLUG = 'Plug'
|
||||
PLUG_MINI_JP = 'Plug Mini (JP)'
|
||||
PLUG_MINI_US = 'Plug Mini (US)'
|
||||
ROBOT_VACUUM_CLEANER_S1 = 'Robot Vacuum Cleaner S1'
|
||||
ROBOT_VACUUM_CLEANER_S1_PLUS = 'Robot Vacuum Cleaner S1 Plus'
|
||||
STRIP_LIGHT = 'Strip Light'
|
|
@ -0,0 +1,157 @@
|
|||
# pylint: disable=too-few-public-methods
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from platypush.context import get_plugin
|
||||
from platypush.entities import Entity
|
||||
|
||||
from ._constants import DeviceType
|
||||
|
||||
|
||||
class EntitySetter(ABC):
|
||||
"""
|
||||
Base class for entity setters.
|
||||
|
||||
The purpose of entity setters is to map property/values passed to
|
||||
:meth:`platypush.plugins.switchbot.SwitchbotPlugin.set_value` to native
|
||||
Switchbot device commands.
|
||||
"""
|
||||
|
||||
def __init__(self, entity: Entity):
|
||||
self.entity = entity
|
||||
self.device_id, self.property = self._plugin._split_device_id_and_property(
|
||||
self.entity.id
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def _set(
|
||||
self,
|
||||
value: Any,
|
||||
*args: Any,
|
||||
property: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
**kwargs: Any,
|
||||
):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
value: Any,
|
||||
*args: Any,
|
||||
property: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
**kwargs: Any,
|
||||
):
|
||||
return self._set(value, *args, property=property, **kwargs)
|
||||
|
||||
@property
|
||||
def _plugin(self):
|
||||
return get_plugin('switchbot')
|
||||
|
||||
|
||||
class EntitySetterWithBinaryState(EntitySetter):
|
||||
"""
|
||||
Base setter for entities with a binary on/off state.
|
||||
"""
|
||||
|
||||
def _set(
|
||||
self,
|
||||
value: Any,
|
||||
*_: Any,
|
||||
property: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
**__: Any,
|
||||
):
|
||||
if property == 'state':
|
||||
action = self._plugin.on if value else self._plugin.off
|
||||
return action(self.device_id)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class EntitySetterWithValueAsMethod(EntitySetter):
|
||||
"""
|
||||
This mapper maps the value passed to
|
||||
:meth:`platypush.plugins.switchbot.SwitchbotPlugin.set_value` to plugin
|
||||
actions.
|
||||
|
||||
In this case, the action value has a 1-1 mapping with the name of the
|
||||
associated plugin action.
|
||||
"""
|
||||
|
||||
def _set(self, value: Any, *_: Any, **__: Any):
|
||||
method = getattr(self._plugin, value, None)
|
||||
assert (
|
||||
method
|
||||
), f'No such action available for device "{self.device_id}": "{value}"'
|
||||
return method(self.device_id)
|
||||
|
||||
|
||||
class CurtainEntitySetter(EntitySetter):
|
||||
"""
|
||||
Curtain entity setter.
|
||||
"""
|
||||
|
||||
def _set(self, value: Any, *_: Any, **__: Any):
|
||||
return self._plugin.set_curtain_position(self.device_id, int(value))
|
||||
|
||||
|
||||
class HumidifierEntitySetter(EntitySetterWithBinaryState):
|
||||
"""
|
||||
Humidifier entity setter.
|
||||
"""
|
||||
|
||||
def _set(
|
||||
self,
|
||||
value: Any,
|
||||
*args: Any,
|
||||
property: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
**kwargs: Any,
|
||||
):
|
||||
if property == 'state':
|
||||
return super()._set(value, *args, property=property, **kwargs)
|
||||
|
||||
if property == 'child_lock':
|
||||
action = self._plugin.lock if value else self._plugin.unlock
|
||||
return action(self.device_id)
|
||||
|
||||
if property in {'auto', 'nebulization_efficiency'}:
|
||||
return self._plugin.set_humidifier_efficiency(self.device_id, value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PlugEntitySetter(EntitySetterWithBinaryState):
|
||||
"""
|
||||
Plug entity setter.
|
||||
"""
|
||||
|
||||
|
||||
class LightEntitySetter(EntitySetter):
|
||||
"""
|
||||
Light entity setter.
|
||||
"""
|
||||
|
||||
def _set(
|
||||
self,
|
||||
value: Any,
|
||||
*_: Any,
|
||||
property: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
**__: Any,
|
||||
):
|
||||
assert property, 'No light property specified'
|
||||
return self._plugin.set_curtain_position(self.device_id, int(value))
|
||||
|
||||
|
||||
# A static map of device types -> entity setters functors.
|
||||
entity_setters: Dict[DeviceType, Type[EntitySetter]] = {
|
||||
DeviceType.BOT: EntitySetterWithValueAsMethod,
|
||||
DeviceType.CEILING_LIGHT: LightEntitySetter,
|
||||
DeviceType.CEILING_LIGHT_PRO: LightEntitySetter,
|
||||
DeviceType.COLOR_BULB: LightEntitySetter,
|
||||
DeviceType.CURTAIN: CurtainEntitySetter,
|
||||
DeviceType.HUMIDIFIER: HumidifierEntitySetter,
|
||||
DeviceType.LOCK: EntitySetterWithValueAsMethod,
|
||||
DeviceType.PLUG: PlugEntitySetter,
|
||||
DeviceType.PLUG_MINI_US: PlugEntitySetter,
|
||||
DeviceType.PLUG_MINI_JP: PlugEntitySetter,
|
||||
DeviceType.STRIP_LIGHT: LightEntitySetter,
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, Union
|
||||
|
||||
from platypush.backend.http.utils import HttpUtils
|
||||
from platypush.config import Config
|
||||
|
@ -22,8 +23,8 @@ class UtilsPlugin(Plugin):
|
|||
_interval_hndl_idx = 0
|
||||
_interval_hndl_idx_lock = threading.RLock()
|
||||
|
||||
_pending_timeouts = {}
|
||||
_pending_intervals = {}
|
||||
_pending_timeouts: Dict[str, Union[Procedure, threading.Timer]] = {}
|
||||
_pending_intervals: Dict[str, Union[Procedure, threading.Thread]] = {}
|
||||
_pending_timeouts_lock = threading.RLock()
|
||||
_pending_intervals_lock = threading.RLock()
|
||||
|
||||
|
@ -67,8 +68,7 @@ class UtilsPlugin(Plugin):
|
|||
if not name:
|
||||
name = self._DEFAULT_TIMEOUT_PREFIX + str(self._timeout_hndl_idx)
|
||||
if name in self._pending_timeouts:
|
||||
return (None,
|
||||
"A timeout named '{}' is already awaiting".format(name))
|
||||
return (None, f"A timeout named '{name}' is already awaiting")
|
||||
|
||||
procedure = Procedure.build(name=name, requests=actions, _async=False)
|
||||
self._pending_timeouts[name] = procedure
|
||||
|
@ -82,10 +82,9 @@ class UtilsPlugin(Plugin):
|
|||
del self._pending_timeouts[name]
|
||||
|
||||
with self._pending_timeouts_lock:
|
||||
self._pending_timeouts[name] = threading.Timer(seconds,
|
||||
_proc_wrapper,
|
||||
args=[procedure],
|
||||
kwargs=args)
|
||||
self._pending_timeouts[name] = threading.Timer(
|
||||
seconds, _proc_wrapper, args=[procedure], kwargs=args
|
||||
)
|
||||
self._pending_timeouts[name].start()
|
||||
|
||||
@action
|
||||
|
@ -98,7 +97,7 @@ class UtilsPlugin(Plugin):
|
|||
"""
|
||||
with self._pending_timeouts_lock:
|
||||
if name not in self._pending_timeouts:
|
||||
self.logger.debug('{} is not a pending timeout'.format(name))
|
||||
self.logger.debug('%s is not a pending timeout', name)
|
||||
return
|
||||
timer = self._pending_timeouts.pop(name)
|
||||
|
||||
|
@ -131,7 +130,8 @@ class UtilsPlugin(Plugin):
|
|||
|
||||
response = {}
|
||||
|
||||
for name in self._pending_timeouts.keys():
|
||||
for name in self._pending_timeouts:
|
||||
# pylint: disable=no-member
|
||||
response[name] = self.get_timeout(name).output.get(name)
|
||||
return response
|
||||
|
||||
|
@ -175,9 +175,7 @@ class UtilsPlugin(Plugin):
|
|||
return {
|
||||
name: {
|
||||
'seconds': timer.interval,
|
||||
'actions': [
|
||||
json.loads(str(a)) for a in timer.args[0].requests
|
||||
]
|
||||
'actions': [json.loads(str(a)) for a in timer.args[0].requests],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,12 +202,10 @@ class UtilsPlugin(Plugin):
|
|||
with self._interval_hndl_idx_lock:
|
||||
self._interval_hndl_idx += 1
|
||||
if not name:
|
||||
name = self._DEFAULT_INTERVAL_PREFIX + \
|
||||
str(self._interval_hndl_idx)
|
||||
name = self._DEFAULT_INTERVAL_PREFIX + str(self._interval_hndl_idx)
|
||||
|
||||
if name in self._pending_intervals:
|
||||
return (None,
|
||||
"An interval named '{}' is already running".format(name))
|
||||
return (None, f"An interval named '{name}' is already running")
|
||||
|
||||
procedure = Procedure.build(name=name, requests=actions, _async=False)
|
||||
self._pending_intervals[name] = procedure
|
||||
|
@ -225,7 +221,8 @@ class UtilsPlugin(Plugin):
|
|||
|
||||
with self._pending_intervals_lock:
|
||||
self._pending_intervals[name] = threading.Thread(
|
||||
target=_proc_wrapper, args=[procedure, seconds], kwargs=args)
|
||||
target=_proc_wrapper, args=[procedure, seconds], kwargs=args
|
||||
)
|
||||
self._pending_intervals[name].start()
|
||||
|
||||
@action
|
||||
|
@ -238,7 +235,7 @@ class UtilsPlugin(Plugin):
|
|||
"""
|
||||
with self._pending_intervals_lock:
|
||||
if name not in self._pending_intervals:
|
||||
self.logger.debug('{} is not a running interval'.format(name))
|
||||
self.logger.debug('%s is not a running interval', name)
|
||||
return
|
||||
del self._pending_intervals[name]
|
||||
|
||||
|
@ -269,7 +266,8 @@ class UtilsPlugin(Plugin):
|
|||
|
||||
response = {}
|
||||
|
||||
for name in self._pending_intervals.keys():
|
||||
for name in self._pending_intervals:
|
||||
# pylint: disable=no-member
|
||||
response[name] = self.get_interval(name).output.get(name)
|
||||
return response
|
||||
|
||||
|
@ -310,13 +308,16 @@ class UtilsPlugin(Plugin):
|
|||
if not timer:
|
||||
return response
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
return {
|
||||
name: {
|
||||
'seconds': timer._args[1],
|
||||
'seconds': timer._args[1], # pylint: disable=protected-access
|
||||
'actions': [
|
||||
json.loads(str(a)) for a in timer._args[0].requests
|
||||
]
|
||||
json.loads(str(a))
|
||||
for a in (
|
||||
# pylint: disable=protected-access
|
||||
timer._args[0].requests
|
||||
)
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,34 +339,10 @@ class UtilsPlugin(Plugin):
|
|||
|
||||
plugins = {}
|
||||
with self._plugins_lock:
|
||||
for name in get_enabled_plugins().keys():
|
||||
for name in get_enabled_plugins():
|
||||
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:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from marshmallow import fields
|
||||
from marshmallow import fields, EXCLUDE
|
||||
from marshmallow.schema import Schema
|
||||
from marshmallow.validate import Range
|
||||
|
||||
|
||||
device_types = [
|
||||
|
@ -47,110 +48,251 @@ remote_types = [
|
|||
]
|
||||
|
||||
|
||||
class ColorField(fields.Field):
|
||||
"""
|
||||
Utility field class for color values.
|
||||
"""
|
||||
|
||||
def _serialize(self, value: str, *_, **__):
|
||||
"""
|
||||
Convert a hex native color value (``ff0000``) to the format exposed by
|
||||
the SwitchBot API (``255:0:0``).
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
# fmt: off
|
||||
return ''.join([f'{int(i):02x}' for i in value.split(':')])
|
||||
|
||||
def _deserialize(self, value: str, *_, **__):
|
||||
"""
|
||||
Convert a SwitchBot API color value (``255:0:0``) to the hex native
|
||||
format (``ff0000``).
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
value = value.lstrip('#')
|
||||
# fmt: off
|
||||
return ':'.join(
|
||||
[str(int(value[i:i+2], 16)) for i in range(0, len(value) - 1, 2)]
|
||||
)
|
||||
|
||||
|
||||
class DeviceSchema(Schema):
|
||||
id = fields.String(attribute='deviceId', required=True, metadata=dict(description='Device unique ID'))
|
||||
name = fields.String(attribute='deviceName', metadata=dict(description='Device name'))
|
||||
"""
|
||||
Base class for SwitchBot device schemas.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Ignore unknown fields.
|
||||
"""
|
||||
|
||||
unknown = EXCLUDE
|
||||
|
||||
id = fields.String(
|
||||
attribute='deviceId',
|
||||
required=True,
|
||||
metadata={'description': 'Device unique ID'},
|
||||
)
|
||||
name = fields.String(
|
||||
attribute='deviceName', metadata={'description': 'Device name'}
|
||||
)
|
||||
device_type = fields.String(
|
||||
attribute='deviceType',
|
||||
metadata=dict(description=f'Default types: [{", ".join(device_types)}]')
|
||||
metadata={'description': f'Default types: [{", ".join(device_types)}]'},
|
||||
)
|
||||
remote_type = fields.String(
|
||||
attribute='remoteType',
|
||||
metadata=dict(description=f'Default types: [{", ".join(remote_types)}]')
|
||||
metadata={'description': f'Default types: [{", ".join(remote_types)}]'},
|
||||
)
|
||||
hub_id = fields.String(
|
||||
attribute='hubDeviceId',
|
||||
metadata={'description': 'Parent hub device unique ID'},
|
||||
)
|
||||
hub_id = fields.String(attribute='hubDeviceId', metadata=dict(description='Parent hub device unique ID'))
|
||||
cloud_service_enabled = fields.Boolean(
|
||||
attribute='enableCloudService',
|
||||
metadata=dict(
|
||||
description='True if cloud access is enabled on this device,'
|
||||
'False otherwise. Only cloud-enabled devices can be '
|
||||
'controlled from the switchbot plugin.'
|
||||
)
|
||||
metadata={
|
||||
'description': 'True if cloud access is enabled on this device,'
|
||||
'False otherwise. Only cloud-enabled devices can be '
|
||||
'controlled from the switchbot plugin.'
|
||||
},
|
||||
)
|
||||
is_calibrated = fields.Boolean(
|
||||
attribute='calibrate',
|
||||
metadata=dict(
|
||||
description='[Curtain devices only] Set to True if the device has been calibrated, False otherwise'
|
||||
)
|
||||
metadata={
|
||||
'description': '[Curtain devices only] Set to True if the device '
|
||||
'has been calibrated, False otherwise'
|
||||
},
|
||||
)
|
||||
open_direction = fields.String(
|
||||
attribute='openDirection',
|
||||
metadata=dict(
|
||||
description='[Curtain devices only] Direction where the curtains will be opened ("left" or "right")'
|
||||
)
|
||||
metadata={
|
||||
'description': '[Curtain devices only] Direction where the curtains '
|
||||
'will be opened ("left" or "right")'
|
||||
},
|
||||
)
|
||||
is_virtual = fields.Boolean(
|
||||
metadata=dict(
|
||||
description='True if this is a virtual device, i.e. a device with an IR remote configuration but not '
|
||||
'managed directly by the Switchbot bridge'
|
||||
)
|
||||
metadata={
|
||||
'description': 'True if this is a virtual device, i.e. a device '
|
||||
'with an IR remote configuration but not managed directly by '
|
||||
'the Switchbot bridge'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DeviceStatusSchema(DeviceSchema):
|
||||
on = fields.Boolean(attribute='power', metadata=dict(description='True if the device is on, False otherwise'))
|
||||
"""
|
||||
Schema for SwitchBot devices status.
|
||||
"""
|
||||
|
||||
on = fields.Boolean(
|
||||
attribute='power',
|
||||
metadata={'description': 'True if the device is on, False otherwise'},
|
||||
)
|
||||
voltage = fields.Float(
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Plug devices only] Voltage of the device, measured '
|
||||
'in volts'
|
||||
},
|
||||
)
|
||||
power = fields.Float(
|
||||
attribute='weight',
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Plug devices only] Consumed power, measured in watts'
|
||||
},
|
||||
)
|
||||
current = fields.Float(
|
||||
attribute='electricCurrent',
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Plug devices only] Device current at the moment, '
|
||||
'measured in amperes'
|
||||
},
|
||||
)
|
||||
active_time = fields.Int(
|
||||
attribute='electricityOfDay',
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Plug devices only] How long the device has been '
|
||||
'absorbing during a day, measured in minutes'
|
||||
},
|
||||
)
|
||||
moving = fields.Boolean(
|
||||
metadata=dict(
|
||||
description='[Curtain devices only] True if the device is moving, False otherwise'
|
||||
)
|
||||
metadata={
|
||||
'description': '[Curtain devices only] True if the device is '
|
||||
'moving, False otherwise'
|
||||
}
|
||||
)
|
||||
position = fields.Int(
|
||||
attribute='slidePosition', metadata=dict(
|
||||
description='[Curtain devices only] Position of the device on the curtain rail, between '
|
||||
'0 (open) and 1 (closed)'
|
||||
)
|
||||
attribute='slidePosition',
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Curtain devices only] Position of the device on '
|
||||
'the curtain rail, between 0% (open) and 100% (closed)'
|
||||
},
|
||||
)
|
||||
locked = fields.Boolean(
|
||||
attribute='lockState',
|
||||
metadata={'description': '[Lock devices only] True if the lock is on'},
|
||||
)
|
||||
door_open = fields.Boolean(
|
||||
attribute='doorState',
|
||||
metadata={
|
||||
'description': '[Lock devices only] True if the door is open, False otherwise'
|
||||
},
|
||||
)
|
||||
brightness = fields.Int(
|
||||
metadata={
|
||||
'description': '[Light devices only] Light brightness, between 1 and 100'
|
||||
},
|
||||
allow_none=True,
|
||||
validate=Range(min=1, max=100),
|
||||
)
|
||||
color = ColorField(
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Light devices only] Color, expressed as a hex string (e.g. FF0000)'
|
||||
},
|
||||
)
|
||||
color_temperature = fields.Int(
|
||||
attribute='colorTemperature',
|
||||
allow_none=True,
|
||||
validate=Range(min=2700, max=6500),
|
||||
metadata={
|
||||
'description': '[Light devices only] Color temperature, between 2700 and 6500'
|
||||
},
|
||||
)
|
||||
temperature = fields.Float(
|
||||
metadata=dict(description='[Meter/humidifier/Air conditioner devices only] Temperature in Celsius')
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Meter/humidifier/Air conditioner devices only] '
|
||||
'Temperature in Celsius'
|
||||
},
|
||||
)
|
||||
humidity = fields.Float(
|
||||
allow_none=True,
|
||||
metadata={'description': '[Meter/humidifier devices only] Humidity in %'},
|
||||
)
|
||||
humidity = fields.Float(metadata=dict(description='[Meter/humidifier devices only] Humidity in %'))
|
||||
fan_speed = fields.Int(
|
||||
metadata=dict(description='[Air conditioner devices only] Speed of the fan')
|
||||
allow_none=True,
|
||||
metadata={'description': '[Air conditioner devices only] Speed of the fan'},
|
||||
)
|
||||
nebulization_efficiency = fields.Float(
|
||||
attribute='nebulizationEfficiency',
|
||||
metadata=dict(
|
||||
description='[Humidifier devices only] Nebulization efficiency in %'
|
||||
)
|
||||
allow_none=True,
|
||||
metadata={
|
||||
'description': '[Humidifier devices only] Nebulization efficiency in %'
|
||||
},
|
||||
)
|
||||
auto = fields.Boolean(
|
||||
metadata=dict(
|
||||
description='[Humidifier devices only] True if auto mode is on'
|
||||
)
|
||||
metadata={'description': '[Humidifier devices only] True if auto mode is on'}
|
||||
)
|
||||
child_lock = fields.Boolean(
|
||||
attribute='childLock',
|
||||
metadata=dict(
|
||||
description='[Humidifier devices only] True if safety lock is on'
|
||||
)
|
||||
metadata={'description': '[Humidifier devices only] True if safety lock is on'},
|
||||
)
|
||||
sound = fields.Boolean(
|
||||
metadata=dict(
|
||||
description='[Humidifier devices only] True if sound is muted'
|
||||
)
|
||||
metadata={'description': '[Humidifier devices only] True if sound is muted'}
|
||||
)
|
||||
low_water = fields.Boolean(
|
||||
attribute='lackWater',
|
||||
metadata={
|
||||
'description': '[Humidifier devices only] True if the device is low on water'
|
||||
},
|
||||
)
|
||||
mode = fields.Int(
|
||||
metadata=dict(description='[Fan/Air conditioner devices only] Fan mode')
|
||||
metadata={'description': '[Fan/Air conditioner devices only] Fan mode'}
|
||||
)
|
||||
speed = fields.Float(
|
||||
metadata=dict(
|
||||
description='[Smart fan devices only] Fan speed, between 1 and 4'
|
||||
)
|
||||
metadata={'description': '[Smart fan devices only] Fan speed, between 1 and 4'}
|
||||
)
|
||||
swinging = fields.Boolean(
|
||||
attribute='shaking',
|
||||
metadata=dict(description='[Smart fan devices only] True if the device is swinging')
|
||||
metadata={
|
||||
'description': '[Smart fan devices only] True if the device is swinging'
|
||||
},
|
||||
)
|
||||
swing_direction = fields.Int(
|
||||
attribute='shakeCenter',
|
||||
metadata=dict(description='[Smart fan devices only] Swing direction')
|
||||
metadata={'description': '[Smart fan devices only] Swing direction'},
|
||||
)
|
||||
swing_range = fields.Float(
|
||||
attribute='shakeRange',
|
||||
metadata=dict(description='[Smart fan devices only] Swing range angle, between 0 and 120')
|
||||
metadata={
|
||||
'description': '[Smart fan devices only] Swing range angle, between 0 and 120'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SceneSchema(Schema):
|
||||
id = fields.String(attribute='sceneId', required=True, metadata=dict(description='Scene ID'))
|
||||
name = fields.String(attribute='sceneName', metadata=dict(description='Scene name'))
|
||||
"""
|
||||
Schema for SwitchBot scenes.
|
||||
"""
|
||||
|
||||
id = fields.String(
|
||||
attribute='sceneId', required=True, metadata={'description': 'Scene ID'}
|
||||
)
|
||||
name = fields.String(attribute='sceneName', metadata={'description': 'Scene name'})
|
||||
|
|
|
@ -8,7 +8,6 @@ import logging
|
|||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import rsa
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
|
@ -17,14 +16,16 @@ from typing import Optional, Tuple, Union
|
|||
|
||||
from dateutil import parser, tz
|
||||
from redis import Redis
|
||||
from rsa.key import PublicKey, PrivateKey
|
||||
from rsa.key import PublicKey, PrivateKey, newkeys
|
||||
|
||||
logger = logging.getLogger('utils')
|
||||
|
||||
|
||||
def get_module_and_method_from_action(action):
|
||||
"""Input : action=music.mpd.play
|
||||
Output : ('music.mpd', 'play')"""
|
||||
"""
|
||||
Input: action=music.mpd.play
|
||||
Output: ('music.mpd', 'play')
|
||||
"""
|
||||
|
||||
tokens = action.split('.')
|
||||
module_name = str.join('.', tokens[:-1])
|
||||
|
@ -38,22 +39,21 @@ def get_message_class_by_type(msgtype):
|
|||
try:
|
||||
module = importlib.import_module('platypush.message.' + msgtype)
|
||||
except ImportError as e:
|
||||
logger.warning('Unsupported message type {}'.format(msgtype))
|
||||
raise RuntimeError(e)
|
||||
logger.warning('Unsupported message type %s', msgtype)
|
||||
raise RuntimeError(e) from e
|
||||
|
||||
cls_name = msgtype[0].upper() + msgtype[1:]
|
||||
|
||||
try:
|
||||
msgclass = getattr(module, cls_name)
|
||||
except AttributeError as e:
|
||||
logger.warning('No such class in {}: {}'.format(module.__name__, cls_name))
|
||||
raise RuntimeError(e)
|
||||
logger.warning('No such class in %s: %s', module.__name__, cls_name)
|
||||
raise RuntimeError(e) from e
|
||||
|
||||
return msgclass
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def get_event_class_by_type(type):
|
||||
def get_event_class_by_type(type): # pylint: disable=redefined-builtin
|
||||
"""Gets an event class by type name"""
|
||||
event_module = importlib.import_module('.'.join(type.split('.')[:-1]))
|
||||
return getattr(event_module, type.split('.')[-1])
|
||||
|
@ -66,7 +66,7 @@ def get_plugin_module_by_name(plugin_name):
|
|||
try:
|
||||
return importlib.import_module('platypush.plugins.' + plugin_name)
|
||||
except ImportError as e:
|
||||
logger.error('Cannot import {}: {}'.format(module_name, str(e)))
|
||||
logger.error('Cannot import %s: %s', module_name, e)
|
||||
return None
|
||||
|
||||
|
||||
|
@ -85,7 +85,7 @@ def get_plugin_class_by_name(plugin_name):
|
|||
module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Cannot import class {}: {}'.format(class_name, str(e)))
|
||||
logger.error('Cannot import class %s: %s', class_name, e)
|
||||
return None
|
||||
|
||||
|
||||
|
@ -191,13 +191,20 @@ def get_decorators(cls, climb_class_hierarchy=False):
|
|||
return decorators
|
||||
|
||||
|
||||
def get_redis_queue_name_by_message(msg):
|
||||
from platypush.message import Message
|
||||
def get_redis_queue_name_by_message(msg) -> Optional[str]:
|
||||
"""
|
||||
Get the Redis queue name for the response(s) associated to a request
|
||||
message.
|
||||
|
||||
if not isinstance(msg, Message):
|
||||
logger.warning('Not a valid message (type: {}): {}'.format(type(msg), msg))
|
||||
:param msg: Input message, as a :class:`platypush.message.request.Request`
|
||||
object.
|
||||
"""
|
||||
from platypush.message.request import Request
|
||||
|
||||
return 'platypush/responses/{}'.format(msg.id) if msg.id else None
|
||||
if not isinstance(msg, Request):
|
||||
logger.warning('Not a valid request (type: %s): %s', type(msg), msg)
|
||||
return None
|
||||
return f'platypush/responses/{msg.id}' if msg.id else None
|
||||
|
||||
|
||||
def _get_ssl_context(
|
||||
|
@ -220,6 +227,9 @@ def _get_ssl_context(
|
|||
|
||||
|
||||
def get_ssl_context(ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None):
|
||||
"""
|
||||
Generic builder for SSL context.
|
||||
"""
|
||||
return _get_ssl_context(
|
||||
context_type=None,
|
||||
ssl_cert=ssl_cert,
|
||||
|
@ -232,6 +242,9 @@ def get_ssl_context(ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=Non
|
|||
def get_ssl_server_context(
|
||||
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None
|
||||
):
|
||||
"""
|
||||
Builder for a server-side SSL context.
|
||||
"""
|
||||
return _get_ssl_context(
|
||||
context_type=ssl.PROTOCOL_TLS_SERVER,
|
||||
ssl_cert=ssl_cert,
|
||||
|
@ -244,6 +257,9 @@ def get_ssl_server_context(
|
|||
def get_ssl_client_context(
|
||||
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None
|
||||
):
|
||||
"""
|
||||
Builder for a client-side SSL context.
|
||||
"""
|
||||
return _get_ssl_context(
|
||||
context_type=ssl.PROTOCOL_TLS_CLIENT,
|
||||
ssl_cert=ssl_cert,
|
||||
|
@ -253,19 +269,22 @@ def get_ssl_client_context(
|
|||
)
|
||||
|
||||
|
||||
def set_thread_name(name):
|
||||
global logger
|
||||
|
||||
def set_thread_name(name: str):
|
||||
"""
|
||||
Set the name of the current thread.
|
||||
"""
|
||||
try:
|
||||
import prctl
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
prctl.set_name(name)
|
||||
prctl.set_name(name) # pylint: disable=no-member
|
||||
except ImportError:
|
||||
logger.debug('Unable to set thread name: prctl module is missing')
|
||||
|
||||
|
||||
def find_bins_in_path(bin_name):
|
||||
"""
|
||||
Search for a binary in the PATH variable.
|
||||
"""
|
||||
return [
|
||||
os.path.join(p, bin_name)
|
||||
for p in os.environ.get('PATH', '').split(':')
|
||||
|
@ -276,14 +295,14 @@ def find_bins_in_path(bin_name):
|
|||
|
||||
def find_files_by_ext(directory, *exts):
|
||||
"""
|
||||
Finds all the files in the given directory with the provided extensions
|
||||
Finds all the files in the given directory with the provided extensions.
|
||||
"""
|
||||
|
||||
if not exts:
|
||||
raise AttributeError('No extensions provided')
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
raise AttributeError('{} is not a valid directory'.format(directory))
|
||||
raise AttributeError(f'{directory} is not a valid directory')
|
||||
|
||||
min_len = len(min(exts, key=len))
|
||||
max_len = len(max(exts, key=len))
|
||||
|
@ -296,7 +315,11 @@ def find_files_by_ext(directory, *exts):
|
|||
return result
|
||||
|
||||
|
||||
def is_process_alive(pid):
|
||||
def is_process_alive(pid: int) -> bool:
|
||||
"""
|
||||
:param pid: Process ID.
|
||||
:return: True if the process with the given PID is alive.
|
||||
"""
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
|
@ -304,7 +327,10 @@ def is_process_alive(pid):
|
|||
return False
|
||||
|
||||
|
||||
def get_ip_or_hostname():
|
||||
def get_ip_or_hostname() -> str:
|
||||
"""
|
||||
Get the the default IP address or hostname of the machine.
|
||||
"""
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
if ip.startswith('127.') or ip.startswith('::1'):
|
||||
try:
|
||||
|
@ -318,11 +344,18 @@ def get_ip_or_hostname():
|
|||
return ip
|
||||
|
||||
|
||||
def get_mime_type(resource):
|
||||
def get_mime_type(resource: str) -> Optional[str]:
|
||||
"""
|
||||
Get the MIME type of the given resource.
|
||||
|
||||
:param resource: The resource to get the MIME type for - it can be a file
|
||||
path or a URL.
|
||||
"""
|
||||
import magic
|
||||
|
||||
if resource.startswith('file://'):
|
||||
resource = resource[len('file://') :]
|
||||
offset = len('file://')
|
||||
resource = resource[offset:]
|
||||
|
||||
# noinspection HttpUrlsUsage
|
||||
if resource.startswith('http://') or resource.startswith('https://'):
|
||||
|
@ -341,8 +374,13 @@ def get_mime_type(resource):
|
|||
if mime:
|
||||
return mime.mime_type if hasattr(mime, 'mime_type') else mime
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def camel_case_to_snake_case(string):
|
||||
"""
|
||||
Utility function to convert CamelCase to snake_case.
|
||||
"""
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
|
@ -364,18 +402,34 @@ def grouper(n, iterable, fillvalue=None):
|
|||
|
||||
|
||||
def is_functional_procedure(obj) -> bool:
|
||||
"""
|
||||
Check if the given object is a functional procedure.
|
||||
"""
|
||||
return callable(obj) and hasattr(obj, 'procedure')
|
||||
|
||||
|
||||
def is_functional_hook(obj) -> bool:
|
||||
"""
|
||||
Check if the given object is a functional hook.
|
||||
"""
|
||||
return callable(obj) and hasattr(obj, 'hook')
|
||||
|
||||
|
||||
def is_functional_cron(obj) -> bool:
|
||||
"""
|
||||
Check if the given object is a functional cron.
|
||||
"""
|
||||
return callable(obj) and hasattr(obj, 'cron') and hasattr(obj, 'cron_expression')
|
||||
|
||||
|
||||
def run(action, *args, **kwargs):
|
||||
"""
|
||||
Run the given action with the given arguments. Example:
|
||||
|
||||
>>> from platypush.utils import run
|
||||
>>> run('music.mpd.play', resource='file:///home/user/music.mp3')
|
||||
|
||||
"""
|
||||
from platypush.context import get_plugin
|
||||
|
||||
(module_name, method_name) = get_module_and_method_from_action(action)
|
||||
|
@ -410,13 +464,13 @@ def generate_rsa_key_pair(
|
|||
:return: A tuple with the generated ``(priv_key_str, pub_key_str)``.
|
||||
"""
|
||||
logger.info('Generating RSA keypair')
|
||||
pub_key, priv_key = rsa.newkeys(size)
|
||||
pub_key, priv_key = newkeys(size)
|
||||
logger.info('Generated RSA keypair')
|
||||
public_key_str = pub_key.save_pkcs1('PEM').decode()
|
||||
private_key_str = priv_key.save_pkcs1('PEM').decode()
|
||||
|
||||
if key_file:
|
||||
logger.info('Saving private key to {}'.format(key_file))
|
||||
logger.info('Saving private key to %s', key_file)
|
||||
with open(os.path.expanduser(key_file), 'w') as f1, open(
|
||||
os.path.expanduser(key_file) + '.pub', 'w'
|
||||
) as f2:
|
||||
|
@ -428,6 +482,9 @@ def generate_rsa_key_pair(
|
|||
|
||||
|
||||
def get_or_generate_jwt_rsa_key_pair():
|
||||
"""
|
||||
Get or generate a JWT RSA key pair.
|
||||
"""
|
||||
from platypush.config import Config
|
||||
|
||||
key_dir = os.path.join(Config.get('workdir'), 'jwt')
|
||||
|
@ -437,8 +494,8 @@ def get_or_generate_jwt_rsa_key_pair():
|
|||
if os.path.isfile(priv_key_file) and os.path.isfile(pub_key_file):
|
||||
with open(pub_key_file, 'r') as f1, open(priv_key_file, 'r') as f2:
|
||||
return (
|
||||
rsa.PublicKey.load_pkcs1(f1.read().encode()),
|
||||
rsa.PrivateKey.load_pkcs1(f2.read().encode()),
|
||||
PublicKey.load_pkcs1(f1.read().encode()),
|
||||
PrivateKey.load_pkcs1(f2.read().encode()),
|
||||
)
|
||||
|
||||
pathlib.Path(key_dir).mkdir(parents=True, exist_ok=True, mode=0o755)
|
||||
|
@ -446,6 +503,12 @@ def get_or_generate_jwt_rsa_key_pair():
|
|||
|
||||
|
||||
def get_enabled_plugins() -> dict:
|
||||
"""
|
||||
Get the enabled plugins.
|
||||
|
||||
:return: A dictionary with the enabled plugins, in the format ``name`` ->
|
||||
:class:`platypush.plugins.Plugin` instance.
|
||||
"""
|
||||
from platypush.config import Config
|
||||
from platypush.context import get_plugin
|
||||
|
||||
|
@ -456,25 +519,39 @@ def get_enabled_plugins() -> dict:
|
|||
if plugin:
|
||||
plugins[name] = plugin
|
||||
except Exception as e:
|
||||
logger.warning(f'Could not initialize plugin {name}')
|
||||
logger.warning('Could not initialize plugin %s', name)
|
||||
logger.exception(e)
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def get_redis() -> Redis:
|
||||
def get_redis(*args, **kwargs) -> Redis:
|
||||
"""
|
||||
Get a Redis client on the basis of the Redis configuration.
|
||||
|
||||
The Redis configuration can be loaded from:
|
||||
|
||||
1. The ``backend.redis`` configuration (``redis_args`` attribute)
|
||||
2. The ``redis`` plugin.
|
||||
|
||||
"""
|
||||
from platypush.config import Config
|
||||
|
||||
return Redis(
|
||||
**(
|
||||
if not (args or kwargs):
|
||||
kwargs = (
|
||||
(Config.get('backend.redis') or {}).get('redis_args', {})
|
||||
or Config.get('redis')
|
||||
or {}
|
||||
)
|
||||
)
|
||||
|
||||
return Redis(*args, **kwargs)
|
||||
|
||||
|
||||
def to_datetime(t: Union[str, int, float, datetime.datetime]) -> datetime.datetime:
|
||||
"""
|
||||
Utility function to convert a datetime/timestamp provided as a
|
||||
string/integer/float/datetime to a ``datetime.datetime`` instance.
|
||||
"""
|
||||
if isinstance(t, (int, float)):
|
||||
return datetime.datetime.fromtimestamp(t, tz=tz.tzutc())
|
||||
if isinstance(t, str):
|
||||
|
|
Loading…
Reference in New Issue