Compare commits

...

15 Commits

Author SHA1 Message Date
Fabio Manganiello fde834c1b1
More LINT fixes + refactors 2023-02-05 22:00:50 +01:00
Fabio Manganiello 4849e14414
LINT fixes for the `utils` module + additional documentation 2023-02-05 18:05:41 +01:00
Fabio Manganiello b8fca97891
Default poll_interval for `RunnablePlugin` set to 30 seconds 2023-02-05 17:31:43 +01:00
Fabio Manganiello 06dfd1a152
Added support for more entities in `switchbot` 2023-02-05 15:34:50 +01:00
Fabio Manganiello 64e9bf17cf
Updated dist files 2023-02-05 14:53:36 +01:00
Fabio Manganiello 2047b9b76c
[WIP] Refactoring `switchbot` plugin as a runnable plugin + entity manager 2023-02-04 22:22:51 +01:00
Fabio Manganiello 65827aa0cd
Updated dist files 2023-02-04 17:36:46 +01:00
Fabio Manganiello b96838a856
Major LINT fixes/refactor for the `Config` class 2023-02-04 17:35:48 +01:00
Fabio Manganiello db5846d296
Add the unit to the `Dimmer` display value if it's available 2023-02-04 17:28:54 +01:00
Fabio Manganiello 0311d87bc3
The `switch.wemo` integration now extends `SwitchEntityManager` 2023-02-04 00:58:28 +01:00
Fabio Manganiello de2849546a
LINT fixes 2023-02-04 00:26:48 +01:00
Fabio Manganiello a160d3217e
Removed legacy `get_sensor_plugins` and `get_switch_plugins` actions 2023-02-03 22:54:42 +01:00
Fabio Manganiello a8fcbef1b5
gitignore 2023-02-03 22:49:50 +01:00
Fabio Manganiello b6814b4f16
Removed legacy Switches integration [frontend] 2023-02-03 22:49:09 +01:00
Fabio Manganiello 6ef2feea71
LINT fixes for `utils` plugin 2023-02-03 18:08:19 +01:00
96 changed files with 1491 additions and 1441 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ platypush/requests
.coverage .coverage
coverage.xml coverage.xml
Session.vim Session.vim
/jsconfig.json
/package.json

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -8,11 +8,11 @@
:error="error" /> :error="error" />
</div> </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 class="name" v-text="value.name" />
</div> </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"> <button @click.stop="collapsed = !collapsed">
<i class="fas" <i class="fas"
:class="{'fa-angle-up': !collapsed, 'fa-angle-down': collapsed}" /> :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) if (this.value?.is_write_only || this.value?.value == null)
return 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 { .value-percent {
font-size: 1.1em; font-size: 1.1em;
font-weight: bold; font-weight: bold;
direction: ltr;
opacity: 0.7; opacity: 0.7;
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1 +0,0 @@
SwitchbotBluetooth

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
import json import json
from typing import Optional from typing import Optional, Union
from redis import Redis from redis import Redis
@ -16,7 +16,7 @@ class RedisBackend(Backend):
and can't post events or requests to the application bus. 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``) :param queue: Queue name to listen on (default: ``platypush_bus_mq``)
:type queue: str :type queue: str
@ -40,12 +40,21 @@ class RedisBackend(Backend):
self.redis_args = redis_args self.redis_args = redis_args
self.redis: Optional[Redis] = None self.redis: Optional[Redis] = None
def send_message(self, msg, queue_name=None, **kwargs): def send_message(
msg = str(msg) self, msg: Union[str, Message], queue_name: Optional[str] = None, **_
if queue_name: ):
self.redis.rpush(queue_name, msg) """
else: Send a message to a Redis queue.
self.redis.rpush(self.queue, msg)
: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): def get_message(self, queue_name=None):
queue = queue_name or self.queue queue = queue_name or self.queue
@ -60,6 +69,7 @@ class RedisBackend(Backend):
self.logger.debug(str(e)) self.logger.debug(str(e))
try: try:
import ast import ast
msg = Message.build(ast.literal_eval(msg)) msg = Message.build(ast.literal_eval(msg))
except Exception as ee: except Exception as ee:
self.logger.debug(str(ee)) self.logger.debug(str(ee))
@ -72,7 +82,11 @@ class RedisBackend(Backend):
def run(self): def run(self):
super().run() 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: with Redis(**self.redis_args) as self.redis:
while not self.should_stop(): while not self.should_stop():
@ -81,7 +95,7 @@ class RedisBackend(Backend):
if not msg: if not msg:
continue 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) self.on_message(msg)
except Exception as e: except Exception as e:
self.logger.exception(e) self.logger.exception(e)

View File

@ -1,48 +1,55 @@
import logging import logging
import threading import threading
from typing import Optional
from redis import Redis
from platypush.bus import Bus from platypush.bus import Bus
from platypush.config import Config
from platypush.message import Message from platypush.message import Message
logger = logging.getLogger('platypush:bus:redis') logger = logging.getLogger('platypush:bus:redis')
class RedisBus(Bus): 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' _DEFAULT_REDIS_QUEUE = 'platypush/bus'
def __init__(self, *args, on_message=None, redis_queue=None, **kwargs): def __init__(self, *args, on_message=None, redis_queue=None, **kwargs):
from platypush.utils import get_redis
super().__init__(on_message=on_message) super().__init__(on_message=on_message)
self.redis = get_redis(*args, **kwargs)
if not args and not kwargs:
kwargs = (Config.get('backend.redis') or {}).get('redis_args', {})
self.redis = Redis(*args, **kwargs)
self.redis_args = kwargs self.redis_args = kwargs
self.redis_queue = redis_queue or self._DEFAULT_REDIS_QUEUE self.redis_queue = redis_queue or self._DEFAULT_REDIS_QUEUE
self.on_message = on_message self.on_message = on_message
self.thread_id = threading.get_ident() self.thread_id = threading.get_ident()
def get(self): def get(self) -> Optional[Message]:
""" Reads one message from the Redis queue """ """
Reads one message from the Redis queue
"""
try: try:
if self.should_stop(): if self.should_stop():
return return None
msg = self.redis.blpop(self.redis_queue, timeout=1) msg = self.redis.blpop(self.redis_queue, timeout=1)
if not msg or msg[1] is None: if not msg or msg[1] is None:
return return None
msg = msg[1].decode('utf-8') msg = msg[1].decode('utf-8')
return Message.build(msg) return Message.build(msg)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
return None
def post(self, msg): 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)) return self.redis.rpush(self.redis_queue, str(msg))
def stop(self): def stop(self):

View File

@ -11,7 +11,7 @@ import shutil
import socket import socket
import sys import sys
from urllib.parse import quote from urllib.parse import quote
from typing import Optional from typing import Optional, Set
import yaml import yaml
@ -22,9 +22,6 @@ from platypush.utils import (
is_functional_cron, is_functional_cron,
) )
""" Config singleton instance """
_default_config_instance = None
class Config: class Config:
""" """
@ -38,16 +35,17 @@ class Config:
Config.get('foo') Config.get('foo')
""" """
""" # Default config file locations:
Default config file locations: # - $HOME/.config/platypush/config.yaml
- $HOME/.config/platypush/config.yaml # - /etc/platypush/config.yaml
- /etc/platypush/config.yaml
"""
_cfgfile_locations = [ _cfgfile_locations = [
os.path.join(os.path.expanduser('~'), '.config', 'platypush', 'config.yaml'), os.path.join(os.path.expanduser('~'), '.config', 'platypush', 'config.yaml'),
os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'), os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'),
] ]
# Config singleton instance
_instance = None
_default_constants = { _default_constants = {
'today': datetime.date.today, 'today': datetime.date.today,
'now': datetime.datetime.now, 'now': datetime.datetime.now,
@ -56,7 +54,7 @@ class Config:
_workdir_location = os.path.join( _workdir_location = os.path.join(
os.path.expanduser('~'), '.local', 'share', 'platypush' os.path.expanduser('~'), '.local', 'share', 'platypush'
) )
_included_files = set() _included_files: Set[str] = set()
def __init__(self, cfgfile=None): def __init__(self, cfgfile=None):
""" """
@ -118,9 +116,8 @@ class Config:
} }
else: else:
db_engine = { db_engine = {
'engine': 'sqlite:///' + os.path.join( 'engine': 'sqlite:///'
quote(self._config['workdir']), 'main.db' + os.path.join(quote(self._config['workdir']), 'main.db')
)
} }
self._config['db'] = db_engine self._config['db'] = db_engine
@ -139,11 +136,7 @@ class Config:
try: try:
os.makedirs(logdir, exist_ok=True) os.makedirs(logdir, exist_ok=True)
except Exception as e: except Exception as e:
print( print(f'Unable to create logs directory {logdir}: {e}')
'Unable to create logs directory {}: {}'.format(
logdir, str(e)
)
)
v = logfile v = logfile
del logging_config['stream'] del logging_config['stream']
@ -214,11 +207,9 @@ class Config:
continue continue
if not os.path.isabs(include_file): if not os.path.isabs(include_file):
include_file = os.path.join(cfgfile_dir, 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) self._included_files.add(include_file)
for incl_section in included_config.keys(): config.update(self._read_config_file(include_file))
config[incl_section] = included_config[incl_section]
elif section == 'scripts_dir': elif section == 'scripts_dir':
assert isinstance(file_config[section], str) assert isinstance(file_config[section], str)
config['scripts_dir'] = os.path.abspath( config['scripts_dir'] = os.path.abspath(
@ -237,11 +228,7 @@ class Config:
try: try:
module = importlib.import_module(modname) module = importlib.import_module(modname)
except Exception as e: except Exception as e:
print( print(f'Unhandled exception while importing module {modname}: {e}')
'Unhandled exception while importing module {}: {}'.format(
modname, str(e)
)
)
return return
prefix = modname + '.' if prefix is None else prefix prefix = modname + '.' if prefix is None else prefix
@ -284,19 +271,19 @@ class Config:
sys.path = sys_path sys.path = sys_path
def _init_components(self): def _init_components(self):
for key in self._config.keys(): for key, component in self._config.items():
if ( if (
key.startswith('backend.') key.startswith('backend.')
and '.'.join(key.split('.')[1:]) in self._backend_manifests and '.'.join(key.split('.')[1:]) in self._backend_manifests
): ):
backend_name = '.'.join(key.split('.')[1:]) backend_name = '.'.join(key.split('.')[1:])
self.backends[backend_name] = self._config[key] self.backends[backend_name] = component
elif key.startswith('event.hook.'): elif key.startswith('event.hook.'):
hook_name = '.'.join(key.split('.')[2:]) hook_name = '.'.join(key.split('.')[2:])
self.event_hooks[hook_name] = self._config[key] self.event_hooks[hook_name] = component
elif key.startswith('cron.'): elif key.startswith('cron.'):
cron_name = '.'.join(key.split('.')[1:]) cron_name = '.'.join(key.split('.')[1:])
self.cronjobs[cron_name] = self._config[key] self.cronjobs[cron_name] = component
elif key.startswith('procedure.'): elif key.startswith('procedure.'):
tokens = key.split('.') tokens = key.split('.')
_async = bool(len(tokens) > 2 and tokens[1] == 'async') _async = bool(len(tokens) > 2 and tokens[1] == 'async')
@ -314,11 +301,11 @@ class Config:
self.procedures[procedure_name] = { self.procedures[procedure_name] = {
'_async': _async, '_async': _async,
'actions': self._config[key], 'actions': component,
'args': args, 'args': args,
} }
elif key in self._plugin_manifests: 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): def _init_manifests(self, base_dir: Optional[str] = None):
if not base_dir: if not base_dir:
@ -353,7 +340,7 @@ class Config:
assert dashboards_dir assert dashboards_dir
abspath = os.path.join(dashboards_dir, name + '.xml') abspath = os.path.join(dashboards_dir, name + '.xml')
if not os.path.isfile(abspath): if not os.path.isfile(abspath):
return return None
with open(abspath, 'r') as fp: with open(abspath, 'r') as fp:
return fp.read() return fp.read()
@ -373,111 +360,94 @@ class Config:
return dashboards return dashboards
@staticmethod @classmethod
def get_dashboard(name: str, dashboards_dir: Optional[str] = None) -> Optional[str]: def _get_instance(
global _default_config_instance cls, cfgfile: Optional[str] = None, force_reload: bool = False
if _default_config_instance is None: ) -> "Config":
_default_config_instance = Config() """
return _default_config_instance._get_dashboard(name, dashboards_dir) 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 @classmethod
def get_dashboards(cls, dashboards_dir: Optional[str] = None) -> dict: def get_dashboards(cls, dashboards_dir: Optional[str] = None) -> dict:
global _default_config_instance # pylint: disable=protected-access
if _default_config_instance is None: return cls._get_instance()._get_dashboards(dashboards_dir)
_default_config_instance = Config()
return _default_config_instance._get_dashboards(dashboards_dir)
def _init_dashboards(self, dashboards_dir: str): def _init_dashboards(self, dashboards_dir: str):
self.dashboards = self._get_dashboards(dashboards_dir) self.dashboards = self._get_dashboards(dashboards_dir)
@staticmethod @classmethod
def get_backends(): def get_backends(cls):
global _default_config_instance return cls._get_instance().backends
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 @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: for location in cls._cfgfile_locations:
if os.path.isfile(location): if os.path.isfile(location):
return location return location
return None
@staticmethod @classmethod
def init(cfgfile=None): def init(cls, cfgfile: Optional[str] = None):
""" """
Initializes the config object singleton Initializes the config object singleton
Params: Params:
cfgfile -- path to the config file - default: _cfgfile_locations cfgfile -- path to the config file - default: _cfgfile_locations
""" """
global _default_config_instance return cls._get_instance(cfgfile, force_reload=True)
_default_config_instance = Config(cfgfile)
@staticmethod @classmethod
def get(key: Optional[str] = None): def get(cls, key: Optional[str] = None):
""" """
Get a config value or the whole configuration object. Get a config value or the whole configuration object.
:param key: Configuration entry to get (default: all entries). :param key: Configuration entry to get (default: all entries).
""" """
global _default_config_instance # pylint: disable=protected-access
if _default_config_instance is None: config = cls._get_instance()._config.copy()
_default_config_instance = Config()
if key: if key:
return _default_config_instance._config.get(key) return config.get(key)
return config
return _default_config_instance._config
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -2,6 +2,7 @@ import asyncio
import importlib import importlib
import logging import logging
from dataclasses import dataclass, field
from threading import RLock from threading import RLock
from typing import Optional, Any from typing import Optional, Any
@ -11,36 +12,62 @@ from ..utils import get_enabled_plugins
logger = logging.getLogger('platypush:context') logger = logging.getLogger('platypush:context')
# Map: backend_name -> backend_instance
backends = {}
# Map: plugin_name -> plugin_instance @dataclass
plugins = {} 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 # Map: plugin_name -> init_lock to make sure that a plugin isn't initialized
# multiple times # multiple times
plugins_init_locks = {} plugins_init_locks = {}
# Reference to the main application bus # 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): 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: Params:
bus -- If specific (it usually should), the messages processed by the bus -- If specific (it usually should), the messages processed by the
backends will be posted on this bus. backends will be posted on this bus.
kwargs -- Any additional key-value parameters required to initialize
kwargs -- Any additional key-value parameters required to initialize the backends the backends
""" """
global main_bus
if bus: if bus:
main_bus = bus _ctx.bus = bus
if global_scope: if global_scope:
global backends backends = _ctx.backends
else: else:
backends = {} backends = {}
@ -57,13 +84,16 @@ def register_backends(bus=None, global_scope=False, **kwargs):
b = getattr(module, cls_name)(bus=bus, **cfg, **kwargs) b = getattr(module, cls_name)(bus=bus, **cfg, **kwargs)
backends[name] = b backends[name] = b
except AttributeError as e: except AttributeError as e:
logger.warning('No such class in {}: {}'.format(module.__name__, cls_name)) logger.warning('No such class in %s: %s', module.__name__, cls_name)
raise RuntimeError(e) raise RuntimeError(e) from e
return backends return backends
def register_plugins(bus=None): def register_plugins(bus=None):
"""
Register and start all the ``RunnablePlugin`` configured implementations.
"""
from ..plugins import RunnablePlugin from ..plugins import RunnablePlugin
for plugin in get_enabled_plugins().values(): for plugin in get_enabled_plugins().values():
@ -75,27 +105,25 @@ def register_plugins(bus=None):
def get_backend(name): def get_backend(name):
"""Returns the backend instance identified by name if it exists""" """Returns the backend instance identified by name if it exists"""
global backends return _ctx.backends.get(name)
return backends.get(name)
def get_plugin(plugin_name, reload=False): def get_plugin(plugin_name, reload=False):
"""Registers a plugin instance by name if not registered already, or """
returns the registered plugin instance""" Registers a plugin instance by name if not registered already, or returns
global plugins the registered plugin instance.
global plugins_init_locks """
if plugin_name not in plugins_init_locks: if plugin_name not in plugins_init_locks:
plugins_init_locks[plugin_name] = RLock() plugins_init_locks[plugin_name] = RLock()
if plugin_name in plugins and not reload: if plugin_name in _ctx.plugins and not reload:
return plugins[plugin_name] return _ctx.plugins[plugin_name]
try: try:
plugin = importlib.import_module('platypush.plugins.' + plugin_name) plugin = importlib.import_module('platypush.plugins.' + plugin_name)
except ImportError as e: except ImportError as e:
logger.warning('No such plugin: {}'.format(plugin_name)) logger.warning('No such plugin: %s', plugin_name)
raise RuntimeError(e) raise RuntimeError(e) from e
# e.g. plugins.music.mpd main class: MusicMpdPlugin # e.g. plugins.music.mpd main class: MusicMpdPlugin
cls_name = '' cls_name = ''
@ -120,30 +148,34 @@ def get_plugin(plugin_name, reload=False):
try: try:
plugin_class = getattr(plugin, cls_name) plugin_class = getattr(plugin, cls_name)
except AttributeError as e: except AttributeError as e:
logger.warning( logger.warning('No such class in %s: %s [error: %s]', plugin_name, cls_name, e)
'No such class in {}: {} [error: {}]'.format(plugin_name, cls_name, str(e)) raise RuntimeError(e) from e
)
raise RuntimeError(e)
with plugins_init_locks[plugin_name]: with plugins_init_locks[plugin_name]:
if plugins.get(plugin_name) and not reload: if _ctx.plugins.get(plugin_name) and not reload:
return plugins[plugin_name] return _ctx.plugins[plugin_name]
plugins[plugin_name] = plugin_class(**plugin_conf) _ctx.plugins[plugin_name] = plugin_class(**plugin_conf)
return plugins[plugin_name] return _ctx.plugins[plugin_name]
def get_bus() -> Bus: def get_bus() -> Bus:
global main_bus """
if main_bus: Get or register the main application bus.
return main_bus """
from platypush.bus.redis import RedisBus 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: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
except (DeprecationWarning, RuntimeError): except (DeprecationWarning, RuntimeError):

View File

@ -35,7 +35,7 @@ class MultiLevelSwitchEntityManager(EntityManager, ABC):
@abstractmethod @abstractmethod
def set_value( # pylint: disable=redefined-builtin def set_value( # pylint: disable=redefined-builtin
self, *entities, property=None, value=None, **__ self, *entities, property=None, data=None, **__
): ):
"""Set a value""" """Set a value"""
raise NotImplementedError() raise NotImplementedError()

View File

@ -5,7 +5,7 @@ import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from functools import wraps from functools import wraps
from typing import Optional from typing import Any, Callable, Optional
from platypush.bus import Bus from platypush.bus import Bus
from platypush.common import ExtensionWithManifest from platypush.common import ExtensionWithManifest
@ -13,12 +13,20 @@ from platypush.event import EventGenerator
from platypush.message.response import Response from platypush.message.response import Response
from platypush.utils import get_decorators, get_plugin_name_by_class, set_thread_name 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) @wraps(f)
def _execute_action(*args, **kwargs): def _execute_action(*args, **kwargs) -> Response:
response = Response() response = Response()
result = f(*args, **kwargs) result = f(*args, **kwargs)
@ -61,7 +69,7 @@ class Plugin(EventGenerator, ExtensionWithManifest): # lgtm [py/missing-call-to
def run(self, method, *args, **kwargs): def run(self, method, *args, **kwargs):
assert ( assert (
method in self.registered_actions 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) return getattr(self, method)(*args, **kwargs)
@ -72,13 +80,13 @@ class RunnablePlugin(Plugin):
def __init__( def __init__(
self, self,
poll_interval: Optional[float] = None, poll_interval: Optional[float] = 30,
stop_timeout: Optional[float] = stop_timeout, stop_timeout: Optional[float] = PLUGIN_STOP_TIMEOUT,
**kwargs, **kwargs,
): ):
""" """
:param poll_interval: How often the :meth:`.loop` function should be :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 :param stop_timeout: How long we should wait for any running
threads/processes to stop before exiting (default: 5 seconds). threads/processes to stop before exiting (default: 5 seconds).
""" """
@ -106,26 +114,26 @@ class RunnablePlugin(Plugin):
def stop(self): def stop(self):
self._should_stop.set() self._should_stop.set()
if self._thread and self._thread.is_alive(): 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: try:
if self._thread: if self._thread:
self._thread.join(timeout=self._stop_timeout) self._thread.join(timeout=self._stop_timeout)
if self._thread and self._thread.is_alive(): if self._thread and self._thread.is_alive():
self.logger.warning( self.logger.warning(
f'Timeout (seconds={self._stop_timeout}) on ' 'Timeout (seconds={%s}) on exit for the plugin %s',
'exit for the plugin ' self._stop_timeout,
+ ( (
get_plugin_name_by_class(self.__class__) get_plugin_name_by_class(self.__class__)
or self.__class__.__name__ or self.__class__.__name__
) ),
) )
except Exception as e: 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): def _runner(self):
self.logger.info(f'Starting {self.__class__.__name__}') self.logger.info('Starting %s', self.__class__.__name__)
while not self.should_stop(): while not self.should_stop():
try: try:

View File

@ -71,7 +71,7 @@ class RedisPlugin(Plugin):
try: try:
return self._get_redis().mset(**kwargs) return self._get_redis().mset(**kwargs)
except TypeError: 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 # broke back-compatibility with the previous way of passing
# key-value pairs to mset directly on kwargs. This try-catch block # key-value pairs to mset directly on kwargs. This try-catch block
# is to support things on all the redis-py versions # is to support things on all the redis-py versions

View File

@ -1,16 +1,17 @@
import contextlib import contextlib
import ipaddress import ipaddress
from typing import List, Optional from typing import Collection, Dict, List, Mapping, Optional, Union
from platypush.plugins import action from platypush.entities import Entity, SwitchEntityManager
from platypush.plugins.switch import SwitchPlugin from platypush.entities.switches import Switch
from platypush.plugins import RunnablePlugin, action
from platypush.utils.workers import Workers from platypush.utils.workers import Workers
from .lib import WemoRunner from .lib import WemoRunner
from .scanner import Scanner from .scanner import Scanner
class SwitchWemoPlugin(SwitchPlugin): class SwitchWemoPlugin(RunnablePlugin, SwitchEntityManager):
""" """
Plugin to control a Belkin WeMo smart switches Plugin to control a Belkin WeMo smart switches
(https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) (https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
@ -20,27 +21,46 @@ class SwitchWemoPlugin(SwitchPlugin):
def __init__( def __init__(
self, self,
devices=None, devices: Optional[Union[Collection[str], Mapping[str, str]]] = None,
netmask: Optional[str] = None, netmask: Optional[str] = None,
port: int = _default_port, port: int = _default_port,
**kwargs **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
This plugin previously used ouimeaux for auto-discovery but it's been dropped because been dropped because:
1. too slow 2. too heavy 3. auto-discovery failed too often.
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 :type devices: list or dict
:param netmask: Alternatively to a list of static IP->name pairs, you can specify the network mask where :param netmask: Alternatively to a list of static IP->name pairs, you
the devices should be scanned (e.g. '192.168.1.0/24') 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) 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.port = port
self.netmask = netmask self.netmask = netmask
self._devices = {} self._devices: Dict[str, str] = {}
self._init_devices(devices) self._init_devices(devices)
def _init_devices(self, devices): def _init_devices(self, devices):
@ -55,34 +75,6 @@ class SwitchWemoPlugin(SwitchPlugin):
self._addresses = set(self._devices.values()) 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: def _get_address(self, device: str) -> str:
if device not in self._addresses: if device not in self._addresses:
with contextlib.suppress(KeyError): with contextlib.suppress(KeyError):
@ -91,8 +83,20 @@ class SwitchWemoPlugin(SwitchPlugin):
return device return device
@action @action
def status(self, device: Optional[str] = None, *_, **__): # pylint: disable=arguments-differ
devices = {device: device} if device else self._devices.copy() 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 = [ ret = [
{ {
@ -104,28 +108,25 @@ class SwitchWemoPlugin(SwitchPlugin):
for (name, addr) in devices.items() for (name, addr) in devices.items()
] ]
self.publish_entities(ret) # type: ignore if publish_entities:
return ret[0] if device else ret self.publish_entities(ret)
return ret
def transform_entities(self, devices: List[dict]): def transform_entities(self, entities: Collection[dict]) -> List[Entity]:
from platypush.entities.switches import Switch return [
Switch(
return super().transform_entities( # type: ignore id=dev["id"],
[ name=dev["name"],
Switch( state=dev["on"],
id=dev["id"], data={
name=dev["name"], "ip": dev["ip"],
state=dev["on"], },
data={ )
"ip": dev["ip"], for dev in (entities or [])
}, ]
)
for dev in (devices or [])
]
)
@action @action
def on(self, device: str, **_): def on(self, device: str, **_): # pylint: disable=arguments-differ
""" """
Turn a switch on Turn a switch on
@ -136,7 +137,7 @@ class SwitchWemoPlugin(SwitchPlugin):
return self.status(device) return self.status(device)
@action @action
def off(self, device: str, **_): def off(self, device: str, **_): # pylint: disable=arguments-differ
""" """
Turn a switch off Turn a switch off
@ -147,7 +148,7 @@ class SwitchWemoPlugin(SwitchPlugin):
return self.status(device) return self.status(device)
@action @action
def toggle(self, device: str, *_, **__): def toggle(self, device: str, *_, **__): # pylint: disable=arguments-differ
""" """
Toggle a device on/off state Toggle a device on/off state
@ -178,7 +179,9 @@ class SwitchWemoPlugin(SwitchPlugin):
return WemoRunner.get_name(device) return WemoRunner.get_name(device)
@action @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 netmask = netmask or self.netmask
assert netmask, "Scan not supported: No netmask specified" 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} devices = {dev.name: dev.addr for dev in workers.responses}
self._init_devices(devices) 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: # vim:sw=4:ts=4:et:

View File

@ -1,35 +1,48 @@
import socket import socket
from dataclasses import dataclass
from typing import Optional from typing import Optional
from platypush.utils.workers import Worker from platypush.utils.workers import Worker
from .lib import WemoRunner from .lib import WemoRunner
@dataclass
class ScanResult: class ScanResult:
def __init__(self, addr: str, name: str, on: bool): """
self.addr = addr Models a scan result.
self.name = name """
self.on = on
addr: str
name: str
on: bool
class Scanner(Worker): class Scanner(Worker):
"""
Worker class used to scan WeMo devices on the network.
"""
timeout = 1.5 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) super().__init__(*args, **kwargs)
self.port = port self.port = port
def process(self, addr: str) -> Optional[ScanResult]: def process(self, msg: str) -> Optional[ScanResult]:
addr = msg
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout) sock.settimeout(self.timeout)
sock.connect((addr, self.port)) sock.connect((addr, self.port))
sock.close() 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: except OSError:
pass return None
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -1,38 +1,71 @@
import queue import queue
import requests
import threading import threading
from typing import List, Optional, Union from typing import Any, Collection, Dict, List, Optional, Tuple, Union
from platypush.plugins import action import requests
from platypush.plugins.switch import SwitchPlugin
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 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 The difference between this plugin and
that the latter acts like a Bluetooth hub/bridge that interacts directly with your Switchbot devices, while this :class:`platypush.plugins.switchbot.bluetooth.SwitchbotBluetoothPlugin` is
plugin requires the devices to be connected to a Switchbot Hub and it controls them through your cloud account. 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: In order to use this plugin:
- Set up a Switchbot Hub and configure your devices through the Switchbot app. - Set up a Switchbot Hub and configure your devices through the
- Follow the steps on the `Switchbot API repo <https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_ Switchbot app.
to get an API token from the 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): def __init__(self, api_token: str, **kwargs):
""" """
:param api_token: API token (see :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) super().__init__(**kwargs)
self._api_token = api_token self._api_token = api_token
self._devices_by_id = {} self._devices_by_id: Dict[str, dict] = {}
self._devices_by_name = {} self._devices_by_name: Dict[str, dict] = {}
@staticmethod @staticmethod
def _url_for(*args, device=None): def _url_for(*args, device=None):
@ -42,6 +75,7 @@ class SwitchbotPlugin(SwitchPlugin):
url += '/'.join(args) url += '/'.join(args)
return url return url
# pylint: disable=keyword-arg-before-vararg
def _run(self, method: str = 'get', *args, device=None, **kwargs): def _run(self, method: str = 'get', *args, device=None, **kwargs):
response = getattr(requests, method)( response = getattr(requests, method)(
self._url_for(*args, device=device), self._url_for(*args, device=device),
@ -50,6 +84,7 @@ class SwitchbotPlugin(SwitchPlugin):
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
timeout=10,
**kwargs, **kwargs,
) )
@ -61,15 +96,22 @@ class SwitchbotPlugin(SwitchPlugin):
return response.get('body') 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: if not use_cache:
self.devices() self.devices()
if device in self._devices_by_id:
return self._devices_by_id[device]
if device in self._devices_by_name: if device in self._devices_by_name:
return self._devices_by_name[device] 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}' assert use_cache, f'Device not found: {device}'
return self._get_device(device, use_cache=False) return self._get_device(device, use_cache=False)
@ -105,28 +147,363 @@ class SwitchbotPlugin(SwitchPlugin):
return devices return devices
def transform_entities(self, devices: List[dict]): @staticmethod
from platypush.entities.switches import Switch 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 @classmethod
[ def _get_device_base(cls, device_dict: dict) -> Device:
Switch( args: Dict[str, Any] = {
id=dev["id"], 'data': cls._get_device_metadata(device_dict),
name=dev["name"], }
state=dev.get("on"),
is_write_only=True, return Device(
data={ id=f'{device_dict["id"]}',
"device_type": dev.get("device_type"), name=f'{device_dict["name"]}',
"is_virtual": dev.get("is_virtual", False), **args,
"hub_id": dev.get("hub_id"),
},
)
for dev in (devices or [])
if dev.get('device_type') == 'Bot'
]
) )
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, self,
q: queue.Queue, q: queue.Queue,
method: str = 'get', method: str = 'get',
@ -153,14 +530,16 @@ class SwitchbotPlugin(SwitchPlugin):
q.put(e) q.put(e)
@action @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. Get the status of all the registered devices or of a specific device.
:param device: Filter by device ID or name. :param device: Filter by device ID or name.
:return: .. schema:: switchbot.DeviceStatusSchema(many=True) :return: .. schema:: switchbot.DeviceStatusSchema(many=True)
""" """
# noinspection PyUnresolvedReferences
devices = self.devices().output devices = self.devices().output
if device: if device:
device_info = self._get_device(device) device_info = self._get_device(device)
@ -175,7 +554,7 @@ class SwitchbotPlugin(SwitchPlugin):
} }
devices_by_id = {dev['id']: dev for dev in devices} devices_by_id = {dev['id']: dev for dev in devices}
queues = [queue.Queue()] * len(devices) queues: List[queue.Queue] = [queue.Queue()] * len(devices)
workers = [ workers = [
threading.Thread( threading.Thread(
target=self._worker, target=self._worker,
@ -205,7 +584,8 @@ class SwitchbotPlugin(SwitchPlugin):
for worker in workers: for worker in workers:
worker.join() worker.join()
self.publish_entities(results) # type: ignore if publish_entities:
self.publish_entities(results)
return results return results
@action @action
@ -215,11 +595,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'press'}) return self._run('post', 'commands', device=dev, json={'command': 'press'})
@action @action
def toggle(self, device: str, **kwargs): def toggle(self, device: str, **_): # pylint: disable=arguments-differ
""" """
Shortcut for :meth:`.press`. Shortcut for :meth:`.press`.
@ -228,29 +608,44 @@ class SwitchbotPlugin(SwitchPlugin):
return self.press(device) return self.press(device)
@action @action
def on(self, device: str, **kwargs): def on(self, device: str, **_): # pylint: disable=arguments-differ
""" """
Send a turn-on command to a device Send a turn-on command to a device
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'turnOn'}) return self._run('post', 'commands', device=dev, json={'command': 'turnOn'})
@action @action
def off(self, device: str, **kwargs): def off(self, device: str, **_): # pylint: disable=arguments-differ
""" """
Send a turn-off command to a device Send a turn-off command to a device
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'turnOff'}) return self._run('post', 'commands', device=dev, json={'command': 'turnOff'})
@property @action
def switches(self) -> List[dict]: def lock(self, device: str, **_):
# noinspection PyUnresolvedReferences """
return [dev for dev in self.status().output if 'on' in dev] 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 @action
def set_curtain_position(self, device: str, position: int): def set_curtain_position(self, device: str, position: int):
@ -260,11 +655,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
:param position: An integer between 0 (open) and 100 (closed). :param position: An integer between 0 (open) and 100 (closed).
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'setPosition', 'command': 'setPosition',
'commandType': 'command', 'commandType': 'command',
@ -278,13 +673,17 @@ class SwitchbotPlugin(SwitchPlugin):
Set the nebulization efficiency of a humidifier device. Set the nebulization efficiency of a humidifier device.
:param device: Device name or ID. :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( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'setMode', 'command': 'setMode',
'commandType': 'command', 'commandType': 'command',
@ -300,7 +699,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
:param speed: Speed between 1 and 4. :param speed: Speed between 1 and 4.
""" """
# noinspection PyUnresolvedReferences
status = self.status(device=device).output status = self.status(device=device).output
mode = status.get('mode') mode = status.get('mode')
swing_range = status.get('swing_range') swing_range = status.get('swing_range')
@ -323,7 +721,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
:param mode: Fan mode (1 or 2). :param mode: Fan mode (1 or 2).
""" """
# noinspection PyUnresolvedReferences
status = self.status(device=device).output status = self.status(device=device).output
speed = status.get('speed') speed = status.get('speed')
swing_range = status.get('swing_range') swing_range = status.get('swing_range')
@ -346,7 +743,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
:param swing_range: Swing range angle, between 0 and 120. :param swing_range: Swing range angle, between 0 and 120.
""" """
# noinspection PyUnresolvedReferences
status = self.status(device=device).output status = self.status(device=device).output
speed = status.get('speed') speed = status.get('speed')
mode = status.get('mode') mode = status.get('mode')
@ -369,7 +765,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
:param temperature: Temperature, in Celsius. :param temperature: Temperature, in Celsius.
""" """
# noinspection PyUnresolvedReferences
status = self.status(device=device).output status = self.status(device=device).output
mode = status.get('mode') mode = status.get('mode')
fan_speed = status.get('fan_speed') fan_speed = status.get('fan_speed')
@ -401,7 +796,6 @@ class SwitchbotPlugin(SwitchPlugin):
* 5: ``heat`` * 5: ``heat``
""" """
# noinspection PyUnresolvedReferences
status = self.status(device=device).output status = self.status(device=device).output
temperature = status.get('temperature') temperature = status.get('temperature')
fan_speed = status.get('fan_speed') fan_speed = status.get('fan_speed')
@ -432,7 +826,6 @@ class SwitchbotPlugin(SwitchPlugin):
* 4: ``high`` * 4: ``high``
""" """
# noinspection PyUnresolvedReferences
status = self.status(device=device).output status = self.status(device=device).output
temperature = status.get('temperature') temperature = status.get('temperature')
mode = status.get('mode') mode = status.get('mode')
@ -457,11 +850,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
:param channel: Channel number. :param channel: Channel number.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'SetChannel', 'command': 'SetChannel',
'commandType': 'command', 'commandType': 'command',
@ -476,11 +869,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'volumeAdd', 'command': 'volumeAdd',
'commandType': 'command', 'commandType': 'command',
@ -494,11 +887,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'volumeSub', 'command': 'volumeSub',
'commandType': 'command', 'commandType': 'command',
@ -512,11 +905,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'setMute', 'command': 'setMute',
'commandType': 'command', 'commandType': 'command',
@ -530,11 +923,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'channelAdd', 'command': 'channelAdd',
'commandType': 'command', 'commandType': 'command',
@ -548,11 +941,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'channelSub', 'command': 'channelSub',
'commandType': 'command', 'commandType': 'command',
@ -566,11 +959,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Play', 'command': 'Play',
'commandType': 'command', 'commandType': 'command',
@ -584,11 +977,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Pause', 'command': 'Pause',
'commandType': 'command', 'commandType': 'command',
@ -596,17 +989,17 @@ class SwitchbotPlugin(SwitchPlugin):
) )
@action @action
def stop(self, device: str): def ir_stop(self, device: str):
""" """
Send stop IR event to a device (for DVD and Speaker). Send stop IR event to a device (for DVD and Speaker).
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Stop', 'command': 'Stop',
'commandType': 'command', 'commandType': 'command',
@ -620,11 +1013,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'FastForward', 'command': 'FastForward',
'commandType': 'command', 'commandType': 'command',
@ -638,11 +1031,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Rewind', 'command': 'Rewind',
'commandType': 'command', 'commandType': 'command',
@ -656,11 +1049,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Next', 'command': 'Next',
'commandType': 'command', 'commandType': 'command',
@ -674,11 +1067,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID. :param device: Device name or ID.
""" """
device = self._get_device(device) dev = self._get_device(device)
return self._run( return self._run(
'post', 'post',
'commands', 'commands',
device=device, device=dev,
json={ json={
'command': 'Previous', 'command': 'Previous',
'commandType': 'command', 'commandType': 'command',
@ -701,7 +1094,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param scene: Scene ID or name. :param scene: Scene ID or name.
""" """
# noinspection PyUnresolvedReferences
scenes = [ scenes = [
s s
for s in self.scenes().output for s in self.scenes().output
@ -711,5 +1103,119 @@ class SwitchbotPlugin(SwitchPlugin):
assert scenes, f'No such scene: {scene}' assert scenes, f'No such scene: {scene}'
return self._run('post', 'scenes', scenes[0]['id'], 'execute') 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: # vim:sw=4:ts=4:et:

View File

@ -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'

View File

@ -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,
}

View File

@ -1,6 +1,7 @@
import json import json
import threading import threading
import time import time
from typing import Dict, Union
from platypush.backend.http.utils import HttpUtils from platypush.backend.http.utils import HttpUtils
from platypush.config import Config from platypush.config import Config
@ -22,8 +23,8 @@ class UtilsPlugin(Plugin):
_interval_hndl_idx = 0 _interval_hndl_idx = 0
_interval_hndl_idx_lock = threading.RLock() _interval_hndl_idx_lock = threading.RLock()
_pending_timeouts = {} _pending_timeouts: Dict[str, Union[Procedure, threading.Timer]] = {}
_pending_intervals = {} _pending_intervals: Dict[str, Union[Procedure, threading.Thread]] = {}
_pending_timeouts_lock = threading.RLock() _pending_timeouts_lock = threading.RLock()
_pending_intervals_lock = threading.RLock() _pending_intervals_lock = threading.RLock()
@ -67,8 +68,7 @@ class UtilsPlugin(Plugin):
if not name: if not name:
name = self._DEFAULT_TIMEOUT_PREFIX + str(self._timeout_hndl_idx) name = self._DEFAULT_TIMEOUT_PREFIX + str(self._timeout_hndl_idx)
if name in self._pending_timeouts: if name in self._pending_timeouts:
return (None, return (None, f"A timeout named '{name}' is already awaiting")
"A timeout named '{}' is already awaiting".format(name))
procedure = Procedure.build(name=name, requests=actions, _async=False) procedure = Procedure.build(name=name, requests=actions, _async=False)
self._pending_timeouts[name] = procedure self._pending_timeouts[name] = procedure
@ -82,10 +82,9 @@ class UtilsPlugin(Plugin):
del self._pending_timeouts[name] del self._pending_timeouts[name]
with self._pending_timeouts_lock: with self._pending_timeouts_lock:
self._pending_timeouts[name] = threading.Timer(seconds, self._pending_timeouts[name] = threading.Timer(
_proc_wrapper, seconds, _proc_wrapper, args=[procedure], kwargs=args
args=[procedure], )
kwargs=args)
self._pending_timeouts[name].start() self._pending_timeouts[name].start()
@action @action
@ -98,7 +97,7 @@ class UtilsPlugin(Plugin):
""" """
with self._pending_timeouts_lock: with self._pending_timeouts_lock:
if name not in self._pending_timeouts: if name not in self._pending_timeouts:
self.logger.debug('{} is not a pending timeout'.format(name)) self.logger.debug('%s is not a pending timeout', name)
return return
timer = self._pending_timeouts.pop(name) timer = self._pending_timeouts.pop(name)
@ -131,7 +130,8 @@ class UtilsPlugin(Plugin):
response = {} 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) response[name] = self.get_timeout(name).output.get(name)
return response return response
@ -175,9 +175,7 @@ class UtilsPlugin(Plugin):
return { return {
name: { name: {
'seconds': timer.interval, 'seconds': timer.interval,
'actions': [ 'actions': [json.loads(str(a)) for a in timer.args[0].requests],
json.loads(str(a)) for a in timer.args[0].requests
]
} }
} }
@ -204,12 +202,10 @@ class UtilsPlugin(Plugin):
with self._interval_hndl_idx_lock: with self._interval_hndl_idx_lock:
self._interval_hndl_idx += 1 self._interval_hndl_idx += 1
if not name: if not name:
name = self._DEFAULT_INTERVAL_PREFIX + \ name = self._DEFAULT_INTERVAL_PREFIX + str(self._interval_hndl_idx)
str(self._interval_hndl_idx)
if name in self._pending_intervals: if name in self._pending_intervals:
return (None, return (None, f"An interval named '{name}' is already running")
"An interval named '{}' is already running".format(name))
procedure = Procedure.build(name=name, requests=actions, _async=False) procedure = Procedure.build(name=name, requests=actions, _async=False)
self._pending_intervals[name] = procedure self._pending_intervals[name] = procedure
@ -225,7 +221,8 @@ class UtilsPlugin(Plugin):
with self._pending_intervals_lock: with self._pending_intervals_lock:
self._pending_intervals[name] = threading.Thread( 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() self._pending_intervals[name].start()
@action @action
@ -238,7 +235,7 @@ class UtilsPlugin(Plugin):
""" """
with self._pending_intervals_lock: with self._pending_intervals_lock:
if name not in self._pending_intervals: if name not in self._pending_intervals:
self.logger.debug('{} is not a running interval'.format(name)) self.logger.debug('%s is not a running interval', name)
return return
del self._pending_intervals[name] del self._pending_intervals[name]
@ -269,7 +266,8 @@ class UtilsPlugin(Plugin):
response = {} 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) response[name] = self.get_interval(name).output.get(name)
return response return response
@ -310,13 +308,16 @@ class UtilsPlugin(Plugin):
if not timer: if not timer:
return response return response
# noinspection PyProtectedMember
return { return {
name: { name: {
'seconds': timer._args[1], 'seconds': timer._args[1], # pylint: disable=protected-access
'actions': [ '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 = {} plugins = {}
with self._plugins_lock: with self._plugins_lock:
for name in get_enabled_plugins().keys(): for name in get_enabled_plugins():
plugins[name] = Config.get(name) plugins[name] = Config.get(name)
return plugins return plugins
@action
def get_sensor_plugins(self) -> dict:
"""
:return: The list of enabled sensor plugins as a ``name -> configuration`` map.
"""
from platypush.plugins.sensor import SensorPlugin
return {
name: Config.get(name)
for name, plugin in get_enabled_plugins().items()
if isinstance(plugin, SensorPlugin)
}
@action
def get_switch_plugins(self) -> dict:
"""
:return: The list of enabled switch plugins as a ``name -> configuration`` map.
"""
from platypush.plugins.switch import SwitchPlugin
return {
name: Config.get(name)
for name, plugin in get_enabled_plugins().items()
if isinstance(plugin, SwitchPlugin)
}
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -1,5 +1,6 @@
from marshmallow import fields from marshmallow import fields, EXCLUDE
from marshmallow.schema import Schema from marshmallow.schema import Schema
from marshmallow.validate import Range
device_types = [ 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): 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( device_type = fields.String(
attribute='deviceType', attribute='deviceType',
metadata=dict(description=f'Default types: [{", ".join(device_types)}]') metadata={'description': f'Default types: [{", ".join(device_types)}]'},
) )
remote_type = fields.String( remote_type = fields.String(
attribute='remoteType', 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( cloud_service_enabled = fields.Boolean(
attribute='enableCloudService', attribute='enableCloudService',
metadata=dict( metadata={
description='True if cloud access is enabled on this device,' 'description': 'True if cloud access is enabled on this device,'
'False otherwise. Only cloud-enabled devices can be ' 'False otherwise. Only cloud-enabled devices can be '
'controlled from the switchbot plugin.' 'controlled from the switchbot plugin.'
) },
) )
is_calibrated = fields.Boolean( is_calibrated = fields.Boolean(
attribute='calibrate', attribute='calibrate',
metadata=dict( metadata={
description='[Curtain devices only] Set to True if the device has been calibrated, False otherwise' 'description': '[Curtain devices only] Set to True if the device '
) 'has been calibrated, False otherwise'
},
) )
open_direction = fields.String( open_direction = fields.String(
attribute='openDirection', attribute='openDirection',
metadata=dict( metadata={
description='[Curtain devices only] Direction where the curtains will be opened ("left" or "right")' 'description': '[Curtain devices only] Direction where the curtains '
) 'will be opened ("left" or "right")'
},
) )
is_virtual = fields.Boolean( is_virtual = fields.Boolean(
metadata=dict( metadata={
description='True if this is a virtual device, i.e. a device with an IR remote configuration but not ' 'description': 'True if this is a virtual device, i.e. a device '
'managed directly by the Switchbot bridge' 'with an IR remote configuration but not managed directly by '
) 'the Switchbot bridge'
}
) )
class DeviceStatusSchema(DeviceSchema): 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( moving = fields.Boolean(
metadata=dict( metadata={
description='[Curtain devices only] True if the device is moving, False otherwise' 'description': '[Curtain devices only] True if the device is '
) 'moving, False otherwise'
}
) )
position = fields.Int( position = fields.Int(
attribute='slidePosition', metadata=dict( attribute='slidePosition',
description='[Curtain devices only] Position of the device on the curtain rail, between ' allow_none=True,
'0 (open) and 1 (closed)' 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( 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( 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( nebulization_efficiency = fields.Float(
attribute='nebulizationEfficiency', attribute='nebulizationEfficiency',
metadata=dict( allow_none=True,
description='[Humidifier devices only] Nebulization efficiency in %' metadata={
) 'description': '[Humidifier devices only] Nebulization efficiency in %'
},
) )
auto = fields.Boolean( auto = fields.Boolean(
metadata=dict( metadata={'description': '[Humidifier devices only] True if auto mode is on'}
description='[Humidifier devices only] True if auto mode is on'
)
) )
child_lock = fields.Boolean( child_lock = fields.Boolean(
attribute='childLock', attribute='childLock',
metadata=dict( metadata={'description': '[Humidifier devices only] True if safety lock is on'},
description='[Humidifier devices only] True if safety lock is on'
)
) )
sound = fields.Boolean( sound = fields.Boolean(
metadata=dict( metadata={'description': '[Humidifier devices only] True if sound is muted'}
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( 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( speed = fields.Float(
metadata=dict( metadata={'description': '[Smart fan devices only] Fan speed, between 1 and 4'}
description='[Smart fan devices only] Fan speed, between 1 and 4'
)
) )
swinging = fields.Boolean( swinging = fields.Boolean(
attribute='shaking', 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( swing_direction = fields.Int(
attribute='shakeCenter', attribute='shakeCenter',
metadata=dict(description='[Smart fan devices only] Swing direction') metadata={'description': '[Smart fan devices only] Swing direction'},
) )
swing_range = fields.Float( swing_range = fields.Float(
attribute='shakeRange', 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): 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'})

View File

@ -8,7 +8,6 @@ import logging
import os import os
import pathlib import pathlib
import re import re
import rsa
import signal import signal
import socket import socket
import ssl import ssl
@ -17,14 +16,16 @@ from typing import Optional, Tuple, Union
from dateutil import parser, tz from dateutil import parser, tz
from redis import Redis from redis import Redis
from rsa.key import PublicKey, PrivateKey from rsa.key import PublicKey, PrivateKey, newkeys
logger = logging.getLogger('utils') logger = logging.getLogger('utils')
def get_module_and_method_from_action(action): 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('.') tokens = action.split('.')
module_name = str.join('.', tokens[:-1]) module_name = str.join('.', tokens[:-1])
@ -38,22 +39,21 @@ def get_message_class_by_type(msgtype):
try: try:
module = importlib.import_module('platypush.message.' + msgtype) module = importlib.import_module('platypush.message.' + msgtype)
except ImportError as e: except ImportError as e:
logger.warning('Unsupported message type {}'.format(msgtype)) logger.warning('Unsupported message type %s', msgtype)
raise RuntimeError(e) raise RuntimeError(e) from e
cls_name = msgtype[0].upper() + msgtype[1:] cls_name = msgtype[0].upper() + msgtype[1:]
try: try:
msgclass = getattr(module, cls_name) msgclass = getattr(module, cls_name)
except AttributeError as e: except AttributeError as e:
logger.warning('No such class in {}: {}'.format(module.__name__, cls_name)) logger.warning('No such class in %s: %s', module.__name__, cls_name)
raise RuntimeError(e) raise RuntimeError(e) from e
return msgclass return msgclass
# noinspection PyShadowingBuiltins def get_event_class_by_type(type): # pylint: disable=redefined-builtin
def get_event_class_by_type(type):
"""Gets an event class by type name""" """Gets an event class by type name"""
event_module = importlib.import_module('.'.join(type.split('.')[:-1])) event_module = importlib.import_module('.'.join(type.split('.')[:-1]))
return getattr(event_module, type.split('.')[-1]) return getattr(event_module, type.split('.')[-1])
@ -66,7 +66,7 @@ def get_plugin_module_by_name(plugin_name):
try: try:
return importlib.import_module('platypush.plugins.' + plugin_name) return importlib.import_module('platypush.plugins.' + plugin_name)
except ImportError as e: except ImportError as e:
logger.error('Cannot import {}: {}'.format(module_name, str(e))) logger.error('Cannot import %s: %s', module_name, e)
return None return None
@ -85,7 +85,7 @@ def get_plugin_class_by_name(plugin_name):
module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin' module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin'
) )
except Exception as e: 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 return None
@ -191,13 +191,20 @@ def get_decorators(cls, climb_class_hierarchy=False):
return decorators return decorators
def get_redis_queue_name_by_message(msg): def get_redis_queue_name_by_message(msg) -> Optional[str]:
from platypush.message import Message """
Get the Redis queue name for the response(s) associated to a request
message.
if not isinstance(msg, Message): :param msg: Input message, as a :class:`platypush.message.request.Request`
logger.warning('Not a valid message (type: {}): {}'.format(type(msg), msg)) 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( 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): 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( return _get_ssl_context(
context_type=None, context_type=None,
ssl_cert=ssl_cert, 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( def get_ssl_server_context(
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None
): ):
"""
Builder for a server-side SSL context.
"""
return _get_ssl_context( return _get_ssl_context(
context_type=ssl.PROTOCOL_TLS_SERVER, context_type=ssl.PROTOCOL_TLS_SERVER,
ssl_cert=ssl_cert, ssl_cert=ssl_cert,
@ -244,6 +257,9 @@ def get_ssl_server_context(
def get_ssl_client_context( def get_ssl_client_context(
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None
): ):
"""
Builder for a client-side SSL context.
"""
return _get_ssl_context( return _get_ssl_context(
context_type=ssl.PROTOCOL_TLS_CLIENT, context_type=ssl.PROTOCOL_TLS_CLIENT,
ssl_cert=ssl_cert, ssl_cert=ssl_cert,
@ -253,19 +269,22 @@ def get_ssl_client_context(
) )
def set_thread_name(name): def set_thread_name(name: str):
global logger """
Set the name of the current thread.
"""
try: try:
import prctl import prctl
# noinspection PyUnresolvedReferences prctl.set_name(name) # pylint: disable=no-member
prctl.set_name(name)
except ImportError: except ImportError:
logger.debug('Unable to set thread name: prctl module is missing') logger.debug('Unable to set thread name: prctl module is missing')
def find_bins_in_path(bin_name): def find_bins_in_path(bin_name):
"""
Search for a binary in the PATH variable.
"""
return [ return [
os.path.join(p, bin_name) os.path.join(p, bin_name)
for p in os.environ.get('PATH', '').split(':') 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): 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: if not exts:
raise AttributeError('No extensions provided') raise AttributeError('No extensions provided')
if not os.path.isdir(directory): 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)) min_len = len(min(exts, key=len))
max_len = len(max(exts, key=len)) max_len = len(max(exts, key=len))
@ -296,7 +315,11 @@ def find_files_by_ext(directory, *exts):
return result 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: try:
os.kill(pid, 0) os.kill(pid, 0)
return True return True
@ -304,7 +327,10 @@ def is_process_alive(pid):
return False 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()) ip = socket.gethostbyname(socket.gethostname())
if ip.startswith('127.') or ip.startswith('::1'): if ip.startswith('127.') or ip.startswith('::1'):
try: try:
@ -318,11 +344,18 @@ def get_ip_or_hostname():
return ip 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 import magic
if resource.startswith('file://'): if resource.startswith('file://'):
resource = resource[len('file://') :] offset = len('file://')
resource = resource[offset:]
# noinspection HttpUrlsUsage # noinspection HttpUrlsUsage
if resource.startswith('http://') or resource.startswith('https://'): if resource.startswith('http://') or resource.startswith('https://'):
@ -341,8 +374,13 @@ def get_mime_type(resource):
if mime: if mime:
return mime.mime_type if hasattr(mime, 'mime_type') else mime return mime.mime_type if hasattr(mime, 'mime_type') else mime
return None
def camel_case_to_snake_case(string): 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) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 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: def is_functional_procedure(obj) -> bool:
"""
Check if the given object is a functional procedure.
"""
return callable(obj) and hasattr(obj, 'procedure') return callable(obj) and hasattr(obj, 'procedure')
def is_functional_hook(obj) -> bool: def is_functional_hook(obj) -> bool:
"""
Check if the given object is a functional hook.
"""
return callable(obj) and hasattr(obj, 'hook') return callable(obj) and hasattr(obj, 'hook')
def is_functional_cron(obj) -> bool: 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') return callable(obj) and hasattr(obj, 'cron') and hasattr(obj, 'cron_expression')
def run(action, *args, **kwargs): 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 from platypush.context import get_plugin
(module_name, method_name) = get_module_and_method_from_action(action) (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)``. :return: A tuple with the generated ``(priv_key_str, pub_key_str)``.
""" """
logger.info('Generating RSA keypair') logger.info('Generating RSA keypair')
pub_key, priv_key = rsa.newkeys(size) pub_key, priv_key = newkeys(size)
logger.info('Generated RSA keypair') logger.info('Generated RSA keypair')
public_key_str = pub_key.save_pkcs1('PEM').decode() public_key_str = pub_key.save_pkcs1('PEM').decode()
private_key_str = priv_key.save_pkcs1('PEM').decode() private_key_str = priv_key.save_pkcs1('PEM').decode()
if key_file: 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( with open(os.path.expanduser(key_file), 'w') as f1, open(
os.path.expanduser(key_file) + '.pub', 'w' os.path.expanduser(key_file) + '.pub', 'w'
) as f2: ) as f2:
@ -428,6 +482,9 @@ def generate_rsa_key_pair(
def get_or_generate_jwt_rsa_key_pair(): def get_or_generate_jwt_rsa_key_pair():
"""
Get or generate a JWT RSA key pair.
"""
from platypush.config import Config from platypush.config import Config
key_dir = os.path.join(Config.get('workdir'), 'jwt') 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): 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: with open(pub_key_file, 'r') as f1, open(priv_key_file, 'r') as f2:
return ( return (
rsa.PublicKey.load_pkcs1(f1.read().encode()), PublicKey.load_pkcs1(f1.read().encode()),
rsa.PrivateKey.load_pkcs1(f2.read().encode()), PrivateKey.load_pkcs1(f2.read().encode()),
) )
pathlib.Path(key_dir).mkdir(parents=True, exist_ok=True, mode=0o755) 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: 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.config import Config
from platypush.context import get_plugin from platypush.context import get_plugin
@ -456,25 +519,39 @@ def get_enabled_plugins() -> dict:
if plugin: if plugin:
plugins[name] = plugin plugins[name] = plugin
except Exception as e: except Exception as e:
logger.warning(f'Could not initialize plugin {name}') logger.warning('Could not initialize plugin %s', name)
logger.exception(e) logger.exception(e)
return plugins 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 from platypush.config import Config
return Redis( if not (args or kwargs):
**( kwargs = (
(Config.get('backend.redis') or {}).get('redis_args', {}) (Config.get('backend.redis') or {}).get('redis_args', {})
or Config.get('redis') or Config.get('redis')
or {} or {}
) )
)
return Redis(*args, **kwargs)
def to_datetime(t: Union[str, int, float, datetime.datetime]) -> datetime.datetime: 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)): if isinstance(t, (int, float)):
return datetime.datetime.fromtimestamp(t, tz=tz.tzutc()) return datetime.datetime.fromtimestamp(t, tz=tz.tzutc())
if isinstance(t, str): if isinstance(t, str):