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.xml
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" />
</div>
<div class="col-s-8 col-m-9 label">
<div class="col-s-7 col-m-8 label">
<div class="name" v-text="value.name" />
</div>
<div class="col-s-3 col-m-2 buttons pull-right">
<div class="col-s-4 col-m-3 buttons pull-right">
<button @click.stop="collapsed = !collapsed">
<i class="fas"
:class="{'fa-angle-up': !collapsed, 'fa-angle-down': collapsed}" />
@ -59,7 +59,10 @@ export default {
if (this.value?.is_write_only || this.value?.value == null)
return null
return this.value.value
let value = this.value.value
if (this.value.unit)
value = `${value} ${this.value.unit}`
return value
}
},
@ -102,6 +105,7 @@ export default {
.value-percent {
font-size: 1.1em;
font-weight: bold;
direction: ltr;
opacity: 0.7;
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -71,7 +71,7 @@ class RedisPlugin(Plugin):
try:
return self._get_redis().mset(**kwargs)
except TypeError:
# XXX commit https://github.com/andymccurdy/redis-py/commit/90a52dd5de111f0053bb3ebaa7c78f73a82a1e3e
# Commit https://github.com/andymccurdy/redis-py/commit/90a52dd5de111f0053bb3ebaa7c78f73a82a1e3e
# broke back-compatibility with the previous way of passing
# key-value pairs to mset directly on kwargs. This try-catch block
# is to support things on all the redis-py versions

View File

@ -1,16 +1,17 @@
import contextlib
import ipaddress
from typing import List, Optional
from typing import Collection, Dict, List, Mapping, Optional, Union
from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin
from platypush.entities import Entity, SwitchEntityManager
from platypush.entities.switches import Switch
from platypush.plugins import RunnablePlugin, action
from platypush.utils.workers import Workers
from .lib import WemoRunner
from .scanner import Scanner
class SwitchWemoPlugin(SwitchPlugin):
class SwitchWemoPlugin(RunnablePlugin, SwitchEntityManager):
"""
Plugin to control a Belkin WeMo smart switches
(https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
@ -20,27 +21,46 @@ class SwitchWemoPlugin(SwitchPlugin):
def __init__(
self,
devices=None,
devices: Optional[Union[Collection[str], Mapping[str, str]]] = None,
netmask: Optional[str] = None,
port: int = _default_port,
**kwargs
):
"""
:param devices: List of IP addresses or name->address map containing the WeMo Switch devices to control.
This plugin previously used ouimeaux for auto-discovery but it's been dropped because
1. too slow 2. too heavy 3. auto-discovery failed too often.
This plugin previously used ``ouimeaux`` for auto-discovery, but it's
been dropped because:
1. Too slow
2. Too heavy
3. Auto-discovery failed too often
However, this also means that you now have to specify either:
- ``devices``: The devices you want to control, as a static list/map
- ``netmask``: The IP netmask that should be scanned for WeMo devices
:param devices: List of IP addresses or name->address map containing
the WeMo Switch devices to control.
:type devices: list or dict
:param netmask: Alternatively to a list of static IP->name pairs, you can specify the network mask where
the devices should be scanned (e.g. '192.168.1.0/24')
:param netmask: Alternatively to a list of static IP->name pairs, you
can specify the network mask where the devices should be scanned
(e.g. '192.168.1.0/24')
:param port: Port where the WeMo devices are expected to expose the RPC/XML over HTTP service (default: 49153)
:param port: Port where the WeMo devices are expected to expose the
RPC/XML over HTTP service (default: 49153)
"""
super().__init__(**kwargs)
assert devices or netmask, (
'Please specify either a static list of devices (either a list of '
'IP addresses or a name->address map) or an IP netmask to scan for '
'devices'
)
self.port = port
self.netmask = netmask
self._devices = {}
self._devices: Dict[str, str] = {}
self._init_devices(devices)
def _init_devices(self, devices):
@ -55,34 +75,6 @@ class SwitchWemoPlugin(SwitchPlugin):
self._addresses = set(self._devices.values())
@property
def switches(self) -> List[dict]:
"""
Get the list of available devices
:returns: The list of devices.
.. code-block:: json
[
{
"ip": "192.168.1.123",
"name": "Switch 1",
"on": true
},
{
"ip": "192.168.1.124",
"name": "Switch 2",
"on": false
}
]
"""
return [
self.status(device).output # type: ignore
for device in self._devices.values()
]
def _get_address(self, device: str) -> str:
if device not in self._addresses:
with contextlib.suppress(KeyError):
@ -91,8 +83,20 @@ class SwitchWemoPlugin(SwitchPlugin):
return device
@action
def status(self, device: Optional[str] = None, *_, **__):
devices = {device: device} if device else self._devices.copy()
# pylint: disable=arguments-differ
def status(
self,
device: Optional[Union[str, Collection[str]]] = None,
publish_entities: bool = True,
**__
) -> List[dict]:
if device:
if isinstance(device, str):
devices = {device: device}
else:
devices = {d: d for d in device}
else:
devices = self._devices.copy()
ret = [
{
@ -104,28 +108,25 @@ class SwitchWemoPlugin(SwitchPlugin):
for (name, addr) in devices.items()
]
self.publish_entities(ret) # type: ignore
return ret[0] if device else ret
if publish_entities:
self.publish_entities(ret)
return ret
def transform_entities(self, devices: List[dict]):
from platypush.entities.switches import Switch
return super().transform_entities( # type: ignore
[
Switch(
id=dev["id"],
name=dev["name"],
state=dev["on"],
data={
"ip": dev["ip"],
},
)
for dev in (devices or [])
]
)
def transform_entities(self, entities: Collection[dict]) -> List[Entity]:
return [
Switch(
id=dev["id"],
name=dev["name"],
state=dev["on"],
data={
"ip": dev["ip"],
},
)
for dev in (entities or [])
]
@action
def on(self, device: str, **_):
def on(self, device: str, **_): # pylint: disable=arguments-differ
"""
Turn a switch on
@ -136,7 +137,7 @@ class SwitchWemoPlugin(SwitchPlugin):
return self.status(device)
@action
def off(self, device: str, **_):
def off(self, device: str, **_): # pylint: disable=arguments-differ
"""
Turn a switch off
@ -147,7 +148,7 @@ class SwitchWemoPlugin(SwitchPlugin):
return self.status(device)
@action
def toggle(self, device: str, *_, **__):
def toggle(self, device: str, *_, **__): # pylint: disable=arguments-differ
"""
Toggle a device on/off state
@ -178,7 +179,9 @@ class SwitchWemoPlugin(SwitchPlugin):
return WemoRunner.get_name(device)
@action
def scan(self, netmask: Optional[str] = None):
def scan(
self, netmask: Optional[str] = None, publish_entities: bool = True
) -> List[dict]:
netmask = netmask or self.netmask
assert netmask, "Scan not supported: No netmask specified"
@ -190,7 +193,33 @@ class SwitchWemoPlugin(SwitchPlugin):
devices = {dev.name: dev.addr for dev in workers.responses}
self._init_devices(devices)
return self.status()
return self.status(publish_entities=publish_entities).output
def main(self):
def scan():
status = (
self.scan(publish_entities=False).output
if not self._devices
else self.status(self._devices.values(), publish_entities=False).output
)
return {dev['ip']: dev for dev in status}
devices = {}
while not self.should_stop():
new_devices = scan()
updated_devices = {
ip: new_devices[ip]
for ip, dev in new_devices.items()
if any(v != devices.get(ip, {}).get(k) for k, v in dev.items())
}
if updated_devices:
self.publish_entities(updated_devices.values())
devices = new_devices
self.wait_stop(self.poll_interval)
# vim:sw=4:ts=4:et:

View File

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

View File

@ -1,38 +1,71 @@
import queue
import requests
import threading
from typing import List, Optional, Union
from typing import Any, Collection, Dict, List, Optional, Tuple, Union
from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin
import requests
from platypush.entities import (
DimmerEntityManager,
EnumSwitchEntityManager,
Entity,
LightEntityManager,
SwitchEntityManager,
)
from platypush.entities.devices import Device
from platypush.entities.dimmers import Dimmer
from platypush.entities.electricity import CurrentSensor, PowerSensor, VoltageSensor
from platypush.entities.lights import Light
from platypush.entities.humidity import HumiditySensor
from platypush.entities.motion import MotionSensor
from platypush.entities.sensors import BinarySensor, EnumSensor, NumericSensor
from platypush.entities.switches import EnumSwitch, Switch
from platypush.entities.temperature import TemperatureSensor
from platypush.plugins import RunnablePlugin, action
from platypush.schemas.switchbot import DeviceSchema, DeviceStatusSchema, SceneSchema
from ._constants import DeviceType
from ._setters import entity_setters
class SwitchbotPlugin(SwitchPlugin):
# pylint: disable=too-many-ancestors
class SwitchbotPlugin(
RunnablePlugin,
DimmerEntityManager,
EnumSwitchEntityManager,
LightEntityManager,
SwitchEntityManager,
):
"""
Plugin to interact with the devices registered to a Switchbot (https://www.switch-bot.com/) account/hub.
Plugin to interact with the devices registered to a Switchbot
(https://www.switch-bot.com/) account/hub.
The difference between this plugin and :class:`platypush.plugins.switchbot.bluetooth.SwitchbotBluetoothPlugin` is
that the latter acts like a Bluetooth hub/bridge that interacts directly with your Switchbot devices, while this
plugin requires the devices to be connected to a Switchbot Hub and it controls them through your cloud account.
The difference between this plugin and
:class:`platypush.plugins.switchbot.bluetooth.SwitchbotBluetoothPlugin` is
that the latter acts like a Bluetooth hub/bridge that interacts directly
with your Switchbot devices, while this plugin requires the devices to be
connected to a Switchbot Hub and it controls them through your cloud
account.
In order to use this plugin:
- Set up a Switchbot Hub and configure your devices through the Switchbot app.
- Follow the steps on the `Switchbot API repo <https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_
to get an API token from the app.
- Set up a Switchbot Hub and configure your devices through the
Switchbot app.
- Follow the steps on the `Switchbot API repo
<https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_ to
get an API token from the app.
"""
def __init__(self, api_token: str, **kwargs):
"""
:param api_token: API token (see
`Getting started with the Switchbot API <https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_).
`Getting started with the Switchbot API
<https://github.com/OpenWonderLabs/SwitchBotAPI#getting-started>`_).
"""
super().__init__(**kwargs)
self._api_token = api_token
self._devices_by_id = {}
self._devices_by_name = {}
self._devices_by_id: Dict[str, dict] = {}
self._devices_by_name: Dict[str, dict] = {}
@staticmethod
def _url_for(*args, device=None):
@ -42,6 +75,7 @@ class SwitchbotPlugin(SwitchPlugin):
url += '/'.join(args)
return url
# pylint: disable=keyword-arg-before-vararg
def _run(self, method: str = 'get', *args, device=None, **kwargs):
response = getattr(requests, method)(
self._url_for(*args, device=device),
@ -50,6 +84,7 @@ class SwitchbotPlugin(SwitchPlugin):
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8',
},
timeout=10,
**kwargs,
)
@ -61,15 +96,22 @@ class SwitchbotPlugin(SwitchPlugin):
return response.get('body')
def _get_device(self, device: str, use_cache=True):
@staticmethod
def _split_device_id_and_property(device: str) -> Tuple[str, Optional[str]]:
tokens = device.split(':')[:2]
return tokens[0], (tokens[1] if len(tokens) == 2 else None)
def _get_device(self, device: str, use_cache=True) -> dict:
if not use_cache:
self.devices()
if device in self._devices_by_id:
return self._devices_by_id[device]
if device in self._devices_by_name:
return self._devices_by_name[device]
device, _ = self._split_device_id_and_property(device)
if device in self._devices_by_id:
return self._devices_by_id[device]
assert use_cache, f'Device not found: {device}'
return self._get_device(device, use_cache=False)
@ -105,28 +147,363 @@ class SwitchbotPlugin(SwitchPlugin):
return devices
def transform_entities(self, devices: List[dict]):
from platypush.entities.switches import Switch
@staticmethod
def _get_device_metadata(device: dict) -> dict:
return {
"device_type": device.get("device_type"),
"is_virtual": device.get("is_virtual", False),
"hub_id": device.get("hub_id"),
}
return super().transform_entities( # type: ignore
[
Switch(
id=dev["id"],
name=dev["name"],
state=dev.get("on"),
is_write_only=True,
data={
"device_type": dev.get("device_type"),
"is_virtual": dev.get("is_virtual", False),
"hub_id": dev.get("hub_id"),
},
)
for dev in (devices or [])
if dev.get('device_type') == 'Bot'
]
@classmethod
def _get_device_base(cls, device_dict: dict) -> Device:
args: Dict[str, Any] = {
'data': cls._get_device_metadata(device_dict),
}
return Device(
id=f'{device_dict["id"]}',
name=f'{device_dict["name"]}',
**args,
)
def _worker(
@staticmethod
def _matches_device_types(device: dict, *device_types: DeviceType) -> bool:
return device.get('device_type') in {
device_type.value for device_type in device_types
}
@classmethod
def _get_bots(cls, *entities: dict) -> List[EnumSwitch]:
return [
EnumSwitch(
id=dev["id"],
name=dev["name"],
value="on" if dev.get("on") else "off",
values=["on", "off", "press"],
is_write_only=True,
data=cls._get_device_metadata(dev),
)
for dev in (entities or [])
if cls._matches_device_types(dev, DeviceType.BOT)
]
@classmethod
def _get_lights(cls, *entities: dict) -> List[Light]:
return [
Light(
id=dev["id"],
name=dev["name"],
on="on" if dev.get("on") else "off",
brightness=dev.get("brightness"),
color_temperature=dev.get("color_temperature"),
color=dev.get("color"),
data=cls._get_device_metadata(dev),
)
for dev in (entities or [])
if cls._matches_device_types(
dev,
DeviceType.CEILING_LIGHT,
DeviceType.CEILING_LIGHT_PRO,
DeviceType.COLOR_BULB,
DeviceType.STRIP_LIGHT,
)
]
@classmethod
def _get_curtains(cls, *entities: dict) -> List[Dimmer]:
return [
Dimmer(
id=dev["id"],
name=dev["name"],
value=dev.get("position"),
min=0,
max=100,
unit='%',
data=cls._get_device_metadata(dev),
)
for dev in (entities or [])
if cls._matches_device_types(dev, DeviceType.CURTAIN)
]
@classmethod
def _get_meters(cls, device_dict: dict) -> List[Device]:
devices = [cls._get_device_base(device_dict)]
if device_dict.get('temperature') is not None:
devices[0].children.append(
TemperatureSensor(
id=f'{device_dict["id"]}:temperature',
name='Temperature',
value=device_dict['temperature'],
unit='C',
)
)
if device_dict.get('humidity') is not None:
devices[0].children.append(
HumiditySensor(
id=f'{device_dict["id"]}:humidity',
name='Humidity',
value=device_dict['humidity'],
min=0,
max=100,
unit='%',
)
)
if not devices[0].children:
return []
return devices
@classmethod
def _get_motion_sensors(cls, device_dict: dict) -> List[Device]:
devices = [cls._get_device_base(device_dict)]
if device_dict.get('moveDetected') is not None:
devices[0].children.append(
MotionSensor(
id=f'{device_dict["id"]}:motion',
name='Motion Detected',
value=bool(device_dict['moveDetected']),
)
)
if device_dict.get('brightness') is not None:
devices[0].children.append(
BinarySensor(
id=f'{device_dict["id"]}:brightness',
name='Bright',
value=device_dict['brightness'] == 'bright',
)
)
if not devices[0].children:
return []
return devices
@classmethod
def _get_contact_sensors(cls, device_dict: dict) -> List[Device]:
devices = cls._get_motion_sensors(device_dict)
if not devices:
return []
if device_dict.get('openState') is not None:
devices[0].children.append(
EnumSensor(
id=f'{device_dict["id"]}:open',
name='Open State',
value=device_dict['openState'],
values=['open', 'close', 'timeOutNotClose'],
)
)
return devices
@classmethod
def _get_sensors(cls, *entities: dict) -> List[Device]:
sensors: List[Entity] = []
for dev in entities:
if cls._matches_device_types(dev, DeviceType.METER, DeviceType.METER_PLUS):
sensors.extend(cls._get_meters(dev))
elif cls._matches_device_types(dev, DeviceType.MOTION_SENSOR):
sensors.extend(cls._get_motion_sensors(dev))
elif cls._matches_device_types(dev, DeviceType.CONTACT_SENSOR):
sensors.extend(cls._get_contact_sensors(dev))
return sensors
@classmethod
def _get_humidifiers(cls, *entities: dict) -> List[Device]:
humidifiers = [
dev
for dev in entities
if cls._matches_device_types(dev, DeviceType.HUMIDIFIER)
]
devs = [Device(**cls._get_device_base(dev)) for dev in humidifiers]
for dev_dict, entity in zip(humidifiers, devs):
if dev_dict.get('power') is not None:
entity.children.append(
Switch(
id=f'{dev_dict["id"]}:state',
name='State',
state=cls._is_on(dev_dict['power']),
)
)
if dev_dict.get('auto') is not None:
entity.children.append(
Switch(
id=f'{dev_dict["id"]}:auto',
name='Automatic Mode',
state=cls._is_on(dev_dict['auto']),
)
)
if dev_dict.get('child_lock') is not None:
entity.children.append(
Switch(
id=f'{dev_dict["id"]}:child_lock',
name='Child Lock',
state=cls._is_on(dev_dict['child_lock']),
)
)
if dev_dict.get('nebulization_efficiency') is not None:
entity.children.append(
Dimmer(
id=f'{dev_dict["id"]}:nebulization_efficiency',
name='Nebulization Efficiency',
value=cls._is_on(dev_dict['nebulization_efficiency']),
min=0,
max=100,
)
)
if dev_dict.get('low_water') is not None:
entity.children.append(
BinarySensor(
id=f'{dev_dict["id"]}:low_water',
name='Low Water',
value=cls._is_on(dev_dict['low_water']),
)
)
if dev_dict.get('temperature') is not None:
entity.children.append(
TemperatureSensor(
id=f'{dev_dict["id"]}:temperature',
name='temperature',
value=dev_dict['temperature'],
)
)
if dev_dict.get('humidity') is not None:
entity.children.append(
HumiditySensor(
id=f'{dev_dict["id"]}:humidity',
name='humidity',
value=dev_dict['humidity'],
)
)
return devs
@classmethod
def _get_locks(cls, *entities: dict) -> List[Device]:
locks = [
dev
for dev in (entities or [])
if cls._matches_device_types(dev, DeviceType.LOCK)
]
devices = [Device(**cls._get_device_base(plug)) for plug in locks]
for plug, device in zip(locks, devices):
if plug.get('locked') is not None:
device.children.append(
Switch(
id=f'{plug["id"]}:locked',
name='Locked',
state=cls._is_on(plug['locked']),
)
)
if plug.get('door_open') is not None:
device.children.append(
BinarySensor(
id=f'{plug["id"]}:door_open',
name='Door Open',
value=cls._is_on(plug['door_open']),
)
)
return devices
@classmethod
def _get_plugs(cls, *entities: dict) -> List[Device]:
plugs = [
dev
for dev in (entities or [])
if cls._matches_device_types(
dev, DeviceType.PLUG, DeviceType.PLUG_MINI_JP, DeviceType.PLUG_MINI_US
)
]
devices = [Device(**cls._get_device_base(plug)) for plug in plugs]
for plug, device in zip(plugs, devices):
if plug.get('on') is not None:
device.children.append(
Switch(
id=f'{plug["id"]}:state',
name='State',
state=cls._is_on(plug['on']),
)
)
if plug.get('power') is not None:
device.children.append(
PowerSensor(
id=f'{plug["id"]}:power',
name='Power',
value=plug['power'],
unit='W',
)
)
if plug.get('voltage') is not None:
device.children.append(
VoltageSensor(
id=f'{plug["id"]}:voltage',
name='Voltage',
value=plug['voltage'],
unit='V',
)
)
if plug.get('current') is not None:
device.children.append(
CurrentSensor(
id=f'{plug["id"]}:current',
name='Current',
value=plug['current'],
unit='A',
)
)
if plug.get('active_time') is not None:
device.children.append(
NumericSensor(
id=f'{plug["id"]}:active_time',
name='Active Time',
value=plug['active_time'],
unit='min',
)
)
return devices
@staticmethod
def _is_on(state: Union[bool, str, int]) -> bool:
if isinstance(state, str):
state = state.lower()
else:
state = bool(state)
return state in {'on', 'true', '1', True}
def transform_entities(self, entities: Collection[dict]) -> Collection[Entity]:
return [
*self._get_bots(*entities),
*self._get_curtains(*entities),
*self._get_humidifiers(*entities),
*self._get_lights(*entities),
*self._get_locks(*entities),
*self._get_plugs(*entities),
*self._get_sensors(*entities),
]
def _worker( # pylint: disable=keyword-arg-before-vararg
self,
q: queue.Queue,
method: str = 'get',
@ -153,14 +530,16 @@ class SwitchbotPlugin(SwitchPlugin):
q.put(e)
@action
def status(self, device: Optional[str] = None) -> Union[dict, List[dict]]:
# pylint: disable=arguments-differ
def status(
self, device: Optional[str] = None, publish_entities: bool = True, **_
) -> Union[dict, List[dict]]:
"""
Get the status of all the registered devices or of a specific device.
:param device: Filter by device ID or name.
:return: .. schema:: switchbot.DeviceStatusSchema(many=True)
"""
# noinspection PyUnresolvedReferences
devices = self.devices().output
if device:
device_info = self._get_device(device)
@ -175,7 +554,7 @@ class SwitchbotPlugin(SwitchPlugin):
}
devices_by_id = {dev['id']: dev for dev in devices}
queues = [queue.Queue()] * len(devices)
queues: List[queue.Queue] = [queue.Queue()] * len(devices)
workers = [
threading.Thread(
target=self._worker,
@ -205,7 +584,8 @@ class SwitchbotPlugin(SwitchPlugin):
for worker in workers:
worker.join()
self.publish_entities(results) # type: ignore
if publish_entities:
self.publish_entities(results)
return results
@action
@ -215,11 +595,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'press'})
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'press'})
@action
def toggle(self, device: str, **kwargs):
def toggle(self, device: str, **_): # pylint: disable=arguments-differ
"""
Shortcut for :meth:`.press`.
@ -228,29 +608,44 @@ class SwitchbotPlugin(SwitchPlugin):
return self.press(device)
@action
def on(self, device: str, **kwargs):
def on(self, device: str, **_): # pylint: disable=arguments-differ
"""
Send a turn-on command to a device
:param device: Device name or ID.
"""
device = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'turnOn'})
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'turnOn'})
@action
def off(self, device: str, **kwargs):
def off(self, device: str, **_): # pylint: disable=arguments-differ
"""
Send a turn-off command to a device
:param device: Device name or ID.
"""
device = self._get_device(device)
return self._run('post', 'commands', device=device, json={'command': 'turnOff'})
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'turnOff'})
@property
def switches(self) -> List[dict]:
# noinspection PyUnresolvedReferences
return [dev for dev in self.status().output if 'on' in dev]
@action
def lock(self, device: str, **_):
"""
Lock a compatible lock device.
:param device: Device name or ID.
"""
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'lock'})
@action
def unlock(self, device: str, **_):
"""
Unlock a compatible lock device.
:param device: Device name or ID.
"""
dev = self._get_device(device)
return self._run('post', 'commands', device=dev, json={'command': 'unlock'})
@action
def set_curtain_position(self, device: str, position: int):
@ -260,11 +655,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
:param position: An integer between 0 (open) and 100 (closed).
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'setPosition',
'commandType': 'command',
@ -278,13 +673,17 @@ class SwitchbotPlugin(SwitchPlugin):
Set the nebulization efficiency of a humidifier device.
:param device: Device name or ID.
:param efficiency: An integer between 0 (open) and 100 (closed) or `auto`.
:param efficiency: Possible values:
- ``auto``: Automatic mode.
- A value between ``0`` and ``100``.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'setMode',
'commandType': 'command',
@ -300,7 +699,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
:param speed: Speed between 1 and 4.
"""
# noinspection PyUnresolvedReferences
status = self.status(device=device).output
mode = status.get('mode')
swing_range = status.get('swing_range')
@ -323,7 +721,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
:param mode: Fan mode (1 or 2).
"""
# noinspection PyUnresolvedReferences
status = self.status(device=device).output
speed = status.get('speed')
swing_range = status.get('swing_range')
@ -346,7 +743,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
:param swing_range: Swing range angle, between 0 and 120.
"""
# noinspection PyUnresolvedReferences
status = self.status(device=device).output
speed = status.get('speed')
mode = status.get('mode')
@ -369,7 +765,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
:param temperature: Temperature, in Celsius.
"""
# noinspection PyUnresolvedReferences
status = self.status(device=device).output
mode = status.get('mode')
fan_speed = status.get('fan_speed')
@ -401,7 +796,6 @@ class SwitchbotPlugin(SwitchPlugin):
* 5: ``heat``
"""
# noinspection PyUnresolvedReferences
status = self.status(device=device).output
temperature = status.get('temperature')
fan_speed = status.get('fan_speed')
@ -432,7 +826,6 @@ class SwitchbotPlugin(SwitchPlugin):
* 4: ``high``
"""
# noinspection PyUnresolvedReferences
status = self.status(device=device).output
temperature = status.get('temperature')
mode = status.get('mode')
@ -457,11 +850,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
:param channel: Channel number.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'SetChannel',
'commandType': 'command',
@ -476,11 +869,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'volumeAdd',
'commandType': 'command',
@ -494,11 +887,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'volumeSub',
'commandType': 'command',
@ -512,11 +905,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'setMute',
'commandType': 'command',
@ -530,11 +923,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'channelAdd',
'commandType': 'command',
@ -548,11 +941,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'channelSub',
'commandType': 'command',
@ -566,11 +959,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'Play',
'commandType': 'command',
@ -584,11 +977,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'Pause',
'commandType': 'command',
@ -596,17 +989,17 @@ class SwitchbotPlugin(SwitchPlugin):
)
@action
def stop(self, device: str):
def ir_stop(self, device: str):
"""
Send stop IR event to a device (for DVD and Speaker).
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'Stop',
'commandType': 'command',
@ -620,11 +1013,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'FastForward',
'commandType': 'command',
@ -638,11 +1031,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'Rewind',
'commandType': 'command',
@ -656,11 +1049,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'Next',
'commandType': 'command',
@ -674,11 +1067,11 @@ class SwitchbotPlugin(SwitchPlugin):
:param device: Device name or ID.
"""
device = self._get_device(device)
dev = self._get_device(device)
return self._run(
'post',
'commands',
device=device,
device=dev,
json={
'command': 'Previous',
'commandType': 'command',
@ -701,7 +1094,6 @@ class SwitchbotPlugin(SwitchPlugin):
:param scene: Scene ID or name.
"""
# noinspection PyUnresolvedReferences
scenes = [
s
for s in self.scenes().output
@ -711,5 +1103,119 @@ class SwitchbotPlugin(SwitchPlugin):
assert scenes, f'No such scene: {scene}'
return self._run('post', 'scenes', scenes[0]['id'], 'execute')
@action
# pylint: disable=redefined-builtin,arguments-differ
def set_value(self, device: str, property=None, data=None, **__):
entity = self._to_entity(device, property)
assert entity, f'No such entity: "{device}"'
dt = entity.data.get('device_type')
assert dt, f'Could not infer the device type for "{device}"'
device_type = DeviceType(dt)
setter_class = entity_setters.get(device_type)
assert setter_class, f'No setters found for device type "{device_type}"'
setter = setter_class(entity)
return setter(property=property, value=data)
def _to_entity(
self,
device: str,
property: Optional[str] = None, # pylint: disable=redefined-builtin
) -> Optional[Entity]:
dev = self._get_device(device)
entities = list(self.transform_entities([dev]))
if not entities:
return None
if len(entities) == 1:
return entities[0]
if not property:
device, property = self._split_device_id_and_property(device)
assert property, 'No property specified'
entity_id = f'{device}:{property}'
return next(iter([e for e in entities if e.id == entity_id]), None)
@action
def set_lights(
self,
*_,
lights: Collection[str],
on: Optional[bool] = None,
brightness: Optional[int] = None,
hex: Optional[str] = None, # pylint: disable=redefined-builtin
temperature: Optional[int] = None,
**__,
):
"""
Change the settings for compatible lights.
:param lights: Light names or IDs.
:param on: Turn on the lights.
:param brightness: Set the brightness of the lights.
:param hex: Set the color of the lights.
:param temperature: Set the temperature of the lights.
"""
devices = [self._get_device(light) for light in lights]
for dev in devices:
if on is not None:
method = self.on if on else self.off
method(dev['id'])
if brightness is not None:
self._run(
'post',
'commands',
device=dev,
json={
'command': 'setBrightness',
'commandType': 'command',
'parameter': brightness,
},
)
if hex is not None:
self._run(
'post',
'commands',
device=dev,
json={
'command': 'setColor',
'commandType': 'command',
'parameter': hex,
},
)
if temperature is not None:
self._run(
'post',
'commands',
device=dev,
json={
'command': 'setColorTemperature',
'commandType': 'command',
'parameter': temperature,
},
)
def main(self):
entities = {}
while not self.should_stop():
status = self.status(publish_entities=False).output
new_entities = {e['id']: e for e in status}
updated_entities = {
id: e
for id, e in new_entities.items()
if any(v != entities.get(id, {}).get(k) for k, v in e.items())
}
if updated_entities:
self.publish_entities(updated_entities.values())
entities = new_entities
self.wait_stop(self.poll_interval)
# vim:sw=4:ts=4:et:

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

View File

@ -1,5 +1,6 @@
from marshmallow import fields
from marshmallow import fields, EXCLUDE
from marshmallow.schema import Schema
from marshmallow.validate import Range
device_types = [
@ -47,110 +48,251 @@ remote_types = [
]
class ColorField(fields.Field):
"""
Utility field class for color values.
"""
def _serialize(self, value: str, *_, **__):
"""
Convert a hex native color value (``ff0000``) to the format exposed by
the SwitchBot API (``255:0:0``).
"""
if not value:
return None
# fmt: off
return ''.join([f'{int(i):02x}' for i in value.split(':')])
def _deserialize(self, value: str, *_, **__):
"""
Convert a SwitchBot API color value (``255:0:0``) to the hex native
format (``ff0000``).
"""
if not value:
return None
value = value.lstrip('#')
# fmt: off
return ':'.join(
[str(int(value[i:i+2], 16)) for i in range(0, len(value) - 1, 2)]
)
class DeviceSchema(Schema):
id = fields.String(attribute='deviceId', required=True, metadata=dict(description='Device unique ID'))
name = fields.String(attribute='deviceName', metadata=dict(description='Device name'))
"""
Base class for SwitchBot device schemas.
"""
class Meta:
"""
Ignore unknown fields.
"""
unknown = EXCLUDE
id = fields.String(
attribute='deviceId',
required=True,
metadata={'description': 'Device unique ID'},
)
name = fields.String(
attribute='deviceName', metadata={'description': 'Device name'}
)
device_type = fields.String(
attribute='deviceType',
metadata=dict(description=f'Default types: [{", ".join(device_types)}]')
metadata={'description': f'Default types: [{", ".join(device_types)}]'},
)
remote_type = fields.String(
attribute='remoteType',
metadata=dict(description=f'Default types: [{", ".join(remote_types)}]')
metadata={'description': f'Default types: [{", ".join(remote_types)}]'},
)
hub_id = fields.String(
attribute='hubDeviceId',
metadata={'description': 'Parent hub device unique ID'},
)
hub_id = fields.String(attribute='hubDeviceId', metadata=dict(description='Parent hub device unique ID'))
cloud_service_enabled = fields.Boolean(
attribute='enableCloudService',
metadata=dict(
description='True if cloud access is enabled on this device,'
'False otherwise. Only cloud-enabled devices can be '
'controlled from the switchbot plugin.'
)
metadata={
'description': 'True if cloud access is enabled on this device,'
'False otherwise. Only cloud-enabled devices can be '
'controlled from the switchbot plugin.'
},
)
is_calibrated = fields.Boolean(
attribute='calibrate',
metadata=dict(
description='[Curtain devices only] Set to True if the device has been calibrated, False otherwise'
)
metadata={
'description': '[Curtain devices only] Set to True if the device '
'has been calibrated, False otherwise'
},
)
open_direction = fields.String(
attribute='openDirection',
metadata=dict(
description='[Curtain devices only] Direction where the curtains will be opened ("left" or "right")'
)
metadata={
'description': '[Curtain devices only] Direction where the curtains '
'will be opened ("left" or "right")'
},
)
is_virtual = fields.Boolean(
metadata=dict(
description='True if this is a virtual device, i.e. a device with an IR remote configuration but not '
'managed directly by the Switchbot bridge'
)
metadata={
'description': 'True if this is a virtual device, i.e. a device '
'with an IR remote configuration but not managed directly by '
'the Switchbot bridge'
}
)
class DeviceStatusSchema(DeviceSchema):
on = fields.Boolean(attribute='power', metadata=dict(description='True if the device is on, False otherwise'))
"""
Schema for SwitchBot devices status.
"""
on = fields.Boolean(
attribute='power',
metadata={'description': 'True if the device is on, False otherwise'},
)
voltage = fields.Float(
allow_none=True,
metadata={
'description': '[Plug devices only] Voltage of the device, measured '
'in volts'
},
)
power = fields.Float(
attribute='weight',
allow_none=True,
metadata={
'description': '[Plug devices only] Consumed power, measured in watts'
},
)
current = fields.Float(
attribute='electricCurrent',
allow_none=True,
metadata={
'description': '[Plug devices only] Device current at the moment, '
'measured in amperes'
},
)
active_time = fields.Int(
attribute='electricityOfDay',
allow_none=True,
metadata={
'description': '[Plug devices only] How long the device has been '
'absorbing during a day, measured in minutes'
},
)
moving = fields.Boolean(
metadata=dict(
description='[Curtain devices only] True if the device is moving, False otherwise'
)
metadata={
'description': '[Curtain devices only] True if the device is '
'moving, False otherwise'
}
)
position = fields.Int(
attribute='slidePosition', metadata=dict(
description='[Curtain devices only] Position of the device on the curtain rail, between '
'0 (open) and 1 (closed)'
)
attribute='slidePosition',
allow_none=True,
metadata={
'description': '[Curtain devices only] Position of the device on '
'the curtain rail, between 0% (open) and 100% (closed)'
},
)
locked = fields.Boolean(
attribute='lockState',
metadata={'description': '[Lock devices only] True if the lock is on'},
)
door_open = fields.Boolean(
attribute='doorState',
metadata={
'description': '[Lock devices only] True if the door is open, False otherwise'
},
)
brightness = fields.Int(
metadata={
'description': '[Light devices only] Light brightness, between 1 and 100'
},
allow_none=True,
validate=Range(min=1, max=100),
)
color = ColorField(
allow_none=True,
metadata={
'description': '[Light devices only] Color, expressed as a hex string (e.g. FF0000)'
},
)
color_temperature = fields.Int(
attribute='colorTemperature',
allow_none=True,
validate=Range(min=2700, max=6500),
metadata={
'description': '[Light devices only] Color temperature, between 2700 and 6500'
},
)
temperature = fields.Float(
metadata=dict(description='[Meter/humidifier/Air conditioner devices only] Temperature in Celsius')
allow_none=True,
metadata={
'description': '[Meter/humidifier/Air conditioner devices only] '
'Temperature in Celsius'
},
)
humidity = fields.Float(
allow_none=True,
metadata={'description': '[Meter/humidifier devices only] Humidity in %'},
)
humidity = fields.Float(metadata=dict(description='[Meter/humidifier devices only] Humidity in %'))
fan_speed = fields.Int(
metadata=dict(description='[Air conditioner devices only] Speed of the fan')
allow_none=True,
metadata={'description': '[Air conditioner devices only] Speed of the fan'},
)
nebulization_efficiency = fields.Float(
attribute='nebulizationEfficiency',
metadata=dict(
description='[Humidifier devices only] Nebulization efficiency in %'
)
allow_none=True,
metadata={
'description': '[Humidifier devices only] Nebulization efficiency in %'
},
)
auto = fields.Boolean(
metadata=dict(
description='[Humidifier devices only] True if auto mode is on'
)
metadata={'description': '[Humidifier devices only] True if auto mode is on'}
)
child_lock = fields.Boolean(
attribute='childLock',
metadata=dict(
description='[Humidifier devices only] True if safety lock is on'
)
metadata={'description': '[Humidifier devices only] True if safety lock is on'},
)
sound = fields.Boolean(
metadata=dict(
description='[Humidifier devices only] True if sound is muted'
)
metadata={'description': '[Humidifier devices only] True if sound is muted'}
)
low_water = fields.Boolean(
attribute='lackWater',
metadata={
'description': '[Humidifier devices only] True if the device is low on water'
},
)
mode = fields.Int(
metadata=dict(description='[Fan/Air conditioner devices only] Fan mode')
metadata={'description': '[Fan/Air conditioner devices only] Fan mode'}
)
speed = fields.Float(
metadata=dict(
description='[Smart fan devices only] Fan speed, between 1 and 4'
)
metadata={'description': '[Smart fan devices only] Fan speed, between 1 and 4'}
)
swinging = fields.Boolean(
attribute='shaking',
metadata=dict(description='[Smart fan devices only] True if the device is swinging')
metadata={
'description': '[Smart fan devices only] True if the device is swinging'
},
)
swing_direction = fields.Int(
attribute='shakeCenter',
metadata=dict(description='[Smart fan devices only] Swing direction')
metadata={'description': '[Smart fan devices only] Swing direction'},
)
swing_range = fields.Float(
attribute='shakeRange',
metadata=dict(description='[Smart fan devices only] Swing range angle, between 0 and 120')
metadata={
'description': '[Smart fan devices only] Swing range angle, between 0 and 120'
},
)
class SceneSchema(Schema):
id = fields.String(attribute='sceneId', required=True, metadata=dict(description='Scene ID'))
name = fields.String(attribute='sceneName', metadata=dict(description='Scene name'))
"""
Schema for SwitchBot scenes.
"""
id = fields.String(
attribute='sceneId', required=True, metadata={'description': 'Scene ID'}
)
name = fields.String(attribute='sceneName', metadata={'description': 'Scene name'})

View File

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