forked from platypush/platypush
Merge pull request '[#255] Model variables as entities' (#256) from 255-model-variables-as-entities into master
Reviewed-on: platypush/platypush#256 Closes: #255
This commit is contained in:
commit
9ebdaf620e
40 changed files with 941 additions and 111 deletions
|
@ -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.9381dbaf.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.11a00465.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.79dede0c.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.5d55c8be.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.d27bc1d7.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.11a00465.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.79dede0c.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.ac3a24a2.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
1
platypush/backend/http/webapp/dist/static/css/8358.b7234311.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/8358.b7234311.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/391-legacy.b0764200.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/391-legacy.b0764200.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/391-legacy.b0764200.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/391-legacy.b0764200.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/391.34877d01.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/391.34877d01.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/391.34877d01.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/391.34877d01.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/8358-legacy.248b1f34.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/8358-legacy.248b1f34.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[8358],{8358:function(e,n,t){t.r(n),t.d(n,{default:function(){return R}});t(8309);var l=t(6252),a=t(3577),r=t(9963),i=function(e){return(0,l.dD)("data-v-0f534cfd"),e=e(),(0,l.Cn)(),e},u={key:0,class:"entity variable-container"},s={class:"col-1 icon"},o={class:"col-s-6 col-m-7 label"},c=["textContent"],d=["textContent"],p={class:"row"},v={class:"row"},f={class:"col-9"},m=["value","disabled"],h={class:"col-3 pull-right"},b=["disabled"],g=i((function(){return(0,l._)("i",{class:"fas fa-trash"},null,-1)})),_=[g],k=["disabled"],y=i((function(){return(0,l._)("i",{class:"fas fa-check"},null,-1)})),w=[y];function C(e,n,t,i,g,y){var C,x=(0,l.up)("EntityIcon");return null!=e.value.value?((0,l.wg)(),(0,l.iD)("div",u,[(0,l._)("div",{class:(0,a.C_)(["head",{collapsed:e.collapsed}])},[(0,l._)("div",s,[(0,l.Wm)(x,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,l._)("div",o,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(e.value.name)},null,8,c)]),(0,l._)("div",{class:"col-s-4 col-m-3 value-container",onClick:n[0]||(n[0]=(0,r.iM)((function(n){return e.collapsed=!e.collapsed}),["stop"]))},[null!=(null===(C=e.value)||void 0===C?void 0:C.value)?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"value",textContent:(0,a.zw)(e.value.value)},null,8,d)):(0,l.kq)("",!0)]),(0,l._)("div",{class:"col-1 collapse-toggler",onClick:n[1]||(n[1]=(0,r.iM)((function(n){return e.collapsed=!e.collapsed}),["stop"]))},[(0,l._)("i",{class:(0,a.C_)(["fas",{"fa-chevron-down":e.collapsed,"fa-chevron-up":!e.collapsed}])},null,2)])],2),e.collapsed?(0,l.kq)("",!0):((0,l.wg)(),(0,l.iD)("div",{key:0,class:"body",onClick:n[4]||(n[4]=(0,r.iM)((function(){return e.prevent&&e.prevent.apply(e,arguments)}),["stop"]))},[(0,l._)("div",p,[(0,l._)("form",{onSubmit:n[3]||(n[3]=(0,r.iM)((function(){return y.setValue&&y.setValue.apply(y,arguments)}),["prevent"]))},[(0,l._)("div",v,[(0,l._)("div",f,[(0,l._)("input",{type:"text",value:e.value.value,placeholder:"Variable value",disabled:e.loading,ref:"text"},null,8,m)]),(0,l._)("div",h,[(0,l._)("button",{type:"button",title:"Clear",onClick:n[2]||(n[2]=(0,r.iM)((function(){return y.clearValue&&y.clearValue.apply(y,arguments)}),["stop"])),disabled:e.loading},_,8,b),(0,l._)("button",{type:"submit",title:"Edit",disabled:e.loading},w,8,k)])])],32)])]))])):(0,l.kq)("",!0)}var x=t(8534),V=(t(5666),t(7909)),q=t(5017),M={name:"Variable",components:{EntityIcon:q["default"]},mixins:[V["default"]],data:function(){return{collapsed:!0}},computed:{isCollapsed:function(){return this.collapsed}},methods:{clearValue: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.$emit("loading",!0),n.prev=1,n.next=4,e.request("variable.unset",{name:e.value.name});case 4:return n.prev=4,e.$emit("loading",!1),n.finish(4);case 7:case"end":return n.stop()}}),n,null,[[1,,4,7]])})))()},setValue:function(){var e=this;return(0,x.Z)(regeneratorRuntime.mark((function n(){var t,l;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(t=e.$refs.text.value,null!==t&&void 0!==t&&t.length){n.next=5;break}return n.next=4,e.clearValue();case 4:return n.abrupt("return",n.sent);case 5:return e.$emit("loading",!0),n.prev=6,l={},l[e.value.name]=t,n.next=11,e.request("variable.set",l);case 11:return n.prev=11,e.$emit("loading",!1),n.finish(11);case 14:case"end":return n.stop()}}),n,null,[[6,,11,14]])})))()}}},$=t(3744);const D=(0,$.Z)(M,[["render",C],["__scopeId","data-v-0f534cfd"]]);var R=D}}]);
|
||||
//# sourceMappingURL=8358-legacy.248b1f34.js.map
|
1
platypush/backend/http/webapp/dist/static/js/8358-legacy.248b1f34.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/8358-legacy.248b1f34.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/8358.c0dcf298.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/8358.c0dcf298.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[8358],{8358:function(l,e,a){a.r(e),a.d(e,{default:function(){return D}});var t=a(6252),s=a(3577),i=a(9963);const n=l=>((0,t.dD)("data-v-0f534cfd"),l=l(),(0,t.Cn)(),l),o={key:0,class:"entity variable-container"},c={class:"col-1 icon"},d={class:"col-s-6 col-m-7 label"},u=["textContent"],r=["textContent"],v={class:"row"},p={class:"row"},f={class:"col-9"},h=["value","disabled"],_={class:"col-3 pull-right"},b=["disabled"],m=n((()=>(0,t._)("i",{class:"fas fa-trash"},null,-1))),y=[m],g=["disabled"],k=n((()=>(0,t._)("i",{class:"fas fa-check"},null,-1))),C=[k];function w(l,e,a,n,m,k){const w=(0,t.up)("EntityIcon");return null!=l.value.value?((0,t.wg)(),(0,t.iD)("div",o,[(0,t._)("div",{class:(0,s.C_)(["head",{collapsed:l.collapsed}])},[(0,t._)("div",c,[(0,t.Wm)(w,{entity:l.value,loading:l.loading,error:l.error},null,8,["entity","loading","error"])]),(0,t._)("div",d,[(0,t._)("div",{class:"name",textContent:(0,s.zw)(l.value.name)},null,8,u)]),(0,t._)("div",{class:"col-s-4 col-m-3 value-container",onClick:e[0]||(e[0]=(0,i.iM)((e=>l.collapsed=!l.collapsed),["stop"]))},[null!=l.value?.value?((0,t.wg)(),(0,t.iD)("span",{key:0,class:"value",textContent:(0,s.zw)(l.value.value)},null,8,r)):(0,t.kq)("",!0)]),(0,t._)("div",{class:"col-1 collapse-toggler",onClick:e[1]||(e[1]=(0,i.iM)((e=>l.collapsed=!l.collapsed),["stop"]))},[(0,t._)("i",{class:(0,s.C_)(["fas",{"fa-chevron-down":l.collapsed,"fa-chevron-up":!l.collapsed}])},null,2)])],2),l.collapsed?(0,t.kq)("",!0):((0,t.wg)(),(0,t.iD)("div",{key:0,class:"body",onClick:e[4]||(e[4]=(0,i.iM)(((...e)=>l.prevent&&l.prevent(...e)),["stop"]))},[(0,t._)("div",v,[(0,t._)("form",{onSubmit:e[3]||(e[3]=(0,i.iM)(((...l)=>k.setValue&&k.setValue(...l)),["prevent"]))},[(0,t._)("div",p,[(0,t._)("div",f,[(0,t._)("input",{type:"text",value:l.value.value,placeholder:"Variable value",disabled:l.loading,ref:"text"},null,8,h)]),(0,t._)("div",_,[(0,t._)("button",{type:"button",title:"Clear",onClick:e[2]||(e[2]=(0,i.iM)(((...l)=>k.clearValue&&k.clearValue(...l)),["stop"])),disabled:l.loading},y,8,b),(0,t._)("button",{type:"submit",title:"Edit",disabled:l.loading},C,8,g)])])],32)])]))])):(0,t.kq)("",!0)}var V=a(7909),x=a(5017),q={name:"Variable",components:{EntityIcon:x["default"]},mixins:[V["default"]],data:function(){return{collapsed:!0}},computed:{isCollapsed(){return this.collapsed}},methods:{async clearValue(){this.$emit("loading",!0);try{await this.request("variable.unset",{name:this.value.name})}finally{this.$emit("loading",!1)}},async setValue(){const l=this.$refs.text.value;if(!l?.length)return await this.clearValue();this.$emit("loading",!0);try{const e={};e[this.value.name]=l,await this.request("variable.set",e)}finally{this.$emit("loading",!1)}}}},M=a(3744);const $=(0,M.Z)(q,[["render",w],["__scopeId","data-v-0f534cfd"]]);var D=$}}]);
|
||||
//# sourceMappingURL=8358.c0dcf298.js.map
|
1
platypush/backend/http/webapp/dist/static/js/8358.c0dcf298.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/8358.c0dcf298.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/app-legacy.ac3a24a2.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/app-legacy.ac3a24a2.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/app.d27bc1d7.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/app.d27bc1d7.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -125,6 +125,9 @@
|
|||
"tv.samsung.ws": {
|
||||
"class": "fas fa-tv"
|
||||
},
|
||||
"variable": {
|
||||
"class": "fas fa-square-root-variable"
|
||||
},
|
||||
"zigbee.mqtt": {
|
||||
"imgUrl": "/icons/zigbee.svg"
|
||||
},
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
<Selector :entity-groups="entityGroups" :value="selector" @input="selector = $event" />
|
||||
</div>
|
||||
|
||||
<div class="col-1 right">
|
||||
<button title="Refresh" @click="refresh">
|
||||
<i class="fa fa-sync-alt" />
|
||||
</button>
|
||||
<div class="col-1 actions-container right">
|
||||
<Dropdown title="Actions" icon-class="fas fa-ellipsis">
|
||||
<DropdownItem icon-class="fa fa-sync-alt" text="Refresh" @click="refresh" />
|
||||
<DropdownItem icon-class="fa fa-square-root-variable"
|
||||
text="Set Variable" @click="variableModalVisible = true" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
@ -26,6 +28,7 @@
|
|||
v-if="modalEntityId && entities[modalEntityId]"
|
||||
/>
|
||||
|
||||
<VariableModal :visible="variableModalVisible" @close="variableModalVisible = false" />
|
||||
<NoItems v-if="!Object.keys(displayGroups || {})?.length">No entities found</NoItems>
|
||||
|
||||
<div class="groups-container" v-else>
|
||||
|
@ -75,6 +78,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from "@/components/elements/Dropdown";
|
||||
import DropdownItem from "@/components/elements/DropdownItem";
|
||||
import Utils from "@/Utils"
|
||||
import Loading from "@/components/Loading";
|
||||
import Icon from "@/components/elements/Icon";
|
||||
|
@ -82,14 +87,25 @@ import NoItems from "@/components/elements/NoItems";
|
|||
import Entity from "./Entity.vue";
|
||||
import Selector from "./Selector.vue";
|
||||
import EntityModal from "./Modal"
|
||||
import VariableModal from "./VariableModal"
|
||||
import { bus } from "@/bus";
|
||||
import icons from '@/assets/icons.json'
|
||||
import meta from './meta.json'
|
||||
|
||||
export default {
|
||||
name: "Entities",
|
||||
components: {Loading, Icon, Entity, Selector, NoItems, EntityModal},
|
||||
mixins: [Utils],
|
||||
components: {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
Entity,
|
||||
EntityModal,
|
||||
Icon,
|
||||
Loading,
|
||||
NoItems,
|
||||
Selector,
|
||||
VariableModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
// Entity scan timeout in seconds
|
||||
|
@ -108,6 +124,7 @@ export default {
|
|||
entities: {},
|
||||
modalEntityId: null,
|
||||
modalVisible: false,
|
||||
variableModalVisible: false,
|
||||
selector: {
|
||||
grouping: 'category',
|
||||
selectedEntities: {},
|
||||
|
@ -205,10 +222,10 @@ export default {
|
|||
const entities = (group ? group.entities : this.entities) || {}
|
||||
const args = {}
|
||||
if (group)
|
||||
args.plugins = Object.keys(entities.reduce((obj, entity) => {
|
||||
args.plugins = Object.values(entities).reduce((obj, entity) => {
|
||||
obj[entity.plugin] = true
|
||||
return obj
|
||||
}, {}))
|
||||
}, {})
|
||||
|
||||
this.loadingEntities = Object.values(entities).reduce((obj, entity) => {
|
||||
if (this._shouldSkipLoading(entity))
|
||||
|
@ -419,12 +436,44 @@ export default {
|
|||
right: 0;
|
||||
text-align: right;
|
||||
margin-right: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
padding-right: 0;
|
||||
|
||||
button {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.right) {
|
||||
.dropdown-container {
|
||||
.dropdown {
|
||||
min-width: 10em;
|
||||
|
||||
.item {
|
||||
box-shadow: none;
|
||||
|
||||
.text {
|
||||
text-align: left;
|
||||
margin-left: 0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: 0;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groups-canvas {
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div class="entity variable-container" v-if="value.value != null">
|
||||
<div class="head" :class="{collapsed: collapsed}">
|
||||
<div class="col-1 icon">
|
||||
<EntityIcon :entity="value" :loading="loading" :error="error" />
|
||||
</div>
|
||||
|
||||
<div class="col-s-6 col-m-7 label">
|
||||
<div class="name" v-text="value.name" />
|
||||
</div>
|
||||
|
||||
<div class="col-s-4 col-m-3 value-container" @click.stop="collapsed = !collapsed">
|
||||
<span class="value" v-text="value.value" v-if="value?.value != null" />
|
||||
</div>
|
||||
|
||||
<div class="col-1 collapse-toggler" @click.stop="collapsed = !collapsed">
|
||||
<i class="fas" :class="{'fa-chevron-down': collapsed, 'fa-chevron-up': !collapsed}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" v-if="!collapsed" @click.stop="prevent">
|
||||
<div class="row">
|
||||
<form @submit.prevent="setValue">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<input type="text" :value="value.value" placeholder="Variable value" :disabled="loading" ref="text" />
|
||||
</div>
|
||||
<div class="col-3 pull-right">
|
||||
<button type="button" title="Clear" @click.stop="clearValue" :disabled="loading">
|
||||
<i class="fas fa-trash" />
|
||||
</button>
|
||||
<button type="submit" title="Edit" :disabled="loading">
|
||||
<i class="fas fa-check" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EntityMixin from "./EntityMixin"
|
||||
import EntityIcon from "./EntityIcon"
|
||||
|
||||
export default {
|
||||
name: 'Variable',
|
||||
components: {EntityIcon},
|
||||
mixins: [EntityMixin],
|
||||
data: function() {
|
||||
return {
|
||||
collapsed: true,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isCollapsed() {
|
||||
return this.collapsed
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async clearValue() {
|
||||
this.$emit('loading', true)
|
||||
try {
|
||||
await this.request('variable.unset', {name: this.value.name})
|
||||
} finally {
|
||||
this.$emit('loading', false)
|
||||
}
|
||||
},
|
||||
|
||||
async setValue() {
|
||||
const value = this.$refs.text.value
|
||||
if (!value?.length)
|
||||
return await this.clearValue()
|
||||
|
||||
this.$emit('loading', true)
|
||||
try {
|
||||
const args = {}
|
||||
args[this.value.name] = value
|
||||
await this.request('variable.set', args)
|
||||
} finally {
|
||||
this.$emit('loading', false)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "common";
|
||||
|
||||
.head .value-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
input[type=text] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<Modal :visible="visible" title="Set Variable" ref="modal" @open="onOpen">
|
||||
<div class="variable-modal-container">
|
||||
<form @submit.prevent="setValue">
|
||||
<div class="row">
|
||||
<div class="col-s-12 col-m-4 label">
|
||||
<label for="name">Variable Name</label>
|
||||
</div>
|
||||
<div class="col-s-12 col-m-8 value">
|
||||
<input type="text" id="variable-name" v-model="varName"
|
||||
placeholder="Variable Name" :disabled="loading" ref="varName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-s-12 col-m-4 label">
|
||||
<label for="name">Variable Value</label>
|
||||
</div>
|
||||
<div class="col-s-12 col-m-8 value">
|
||||
<input type="text" id="variable-value" v-model="varValue" ref="varValue"
|
||||
placeholder="Variable Value" :disabled="loading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row button-container">
|
||||
<button type="submit" title="Set" :disabled="loading">
|
||||
<i class="fas fa-check" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/Modal";
|
||||
import Utils from "@/Utils";
|
||||
|
||||
export default {
|
||||
name: "VariableModal",
|
||||
components: {Modal},
|
||||
mixins: [Utils],
|
||||
emits: ['close'],
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
varName: null,
|
||||
varValue: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async clearValue() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('variable.unset', {name: this.varName.trim()})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async setValue() {
|
||||
const varName = this.varName.trim()
|
||||
if (!varName?.length) {
|
||||
this.notifyWarning('No variable name has been specified')
|
||||
}
|
||||
|
||||
const value = this.varValue
|
||||
if (!value?.length) {
|
||||
await this.clearValue()
|
||||
} else {
|
||||
this.loading = true
|
||||
try {
|
||||
const args = {}
|
||||
args[varName] = value
|
||||
await this.request('variable.set', args)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
this.$refs.varName.value = ''
|
||||
this.$refs.varValue.value = ''
|
||||
this.$refs.modal.close()
|
||||
},
|
||||
|
||||
onOpen() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.varName.focus()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "common";
|
||||
|
||||
.variable-modal-container {
|
||||
form {
|
||||
padding: 1em 0;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 0.25em 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: -0.75em;
|
||||
padding-top: 0.5em;
|
||||
border-top: 1px solid $border-color-1;
|
||||
|
||||
button {
|
||||
min-width: 10em;
|
||||
background: none;
|
||||
border-radius: 1.5em;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include from($tablet) {
|
||||
.value {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -343,6 +343,14 @@
|
|||
}
|
||||
},
|
||||
|
||||
"variable": {
|
||||
"name": "Variable",
|
||||
"name_plural": "Variables",
|
||||
"icon": {
|
||||
"class": "fas fa-square-root-variable"
|
||||
}
|
||||
},
|
||||
|
||||
"voltage_sensor": {
|
||||
"name": "Sensor",
|
||||
"name_plural": "Sensors",
|
||||
|
|
|
@ -11,7 +11,7 @@ import shutil
|
|||
import socket
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
from typing import Optional, Set
|
||||
from typing import Dict, Optional, Set
|
||||
|
||||
import yaml
|
||||
|
||||
|
@ -129,7 +129,7 @@ class Config:
|
|||
}
|
||||
|
||||
if 'logging' in self._config:
|
||||
for (k, v) in self._config['logging'].items():
|
||||
for k, v in self._config['logging'].items():
|
||||
if k == 'filename':
|
||||
logfile = os.path.expanduser(v)
|
||||
logdir = os.path.dirname(logfile)
|
||||
|
@ -158,7 +158,7 @@ class Config:
|
|||
os.environ[k] = str(v)
|
||||
|
||||
self.backends = {}
|
||||
self.plugins = {}
|
||||
self.plugins = self._core_plugins
|
||||
self.event_hooks = {}
|
||||
self.procedures = {}
|
||||
self.constants = {}
|
||||
|
@ -173,6 +173,12 @@ class Config:
|
|||
self._init_components()
|
||||
self._init_dashboards(self._config['dashboards_dir'])
|
||||
|
||||
@property
|
||||
def _core_plugins(self) -> Dict[str, dict]:
|
||||
return {
|
||||
'variable': {},
|
||||
}
|
||||
|
||||
def _create_default_config(self):
|
||||
cfg_mod_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
cfgfile = self._cfgfile_locations[0]
|
||||
|
@ -330,7 +336,7 @@ class Config:
|
|||
if 'constants' in self._config:
|
||||
self.constants = self._config['constants']
|
||||
|
||||
for (key, value) in self._default_constants.items():
|
||||
for key, value in self._default_constants.items():
|
||||
self.constants[key] = value
|
||||
|
||||
def _get_dashboard(
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import logging
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import types
|
||||
from datetime import datetime
|
||||
import pkgutil
|
||||
from typing import Callable, Dict, Final, Optional, Set, Type, Tuple, Any
|
||||
|
||||
import pkgutil
|
||||
|
||||
from dateutil.tz import tzutc
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
|
@ -23,6 +27,7 @@ from sqlalchemy import (
|
|||
from sqlalchemy.orm import ColumnProperty, backref, relationship
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
|
||||
import platypush
|
||||
from platypush.common.db import Base
|
||||
from platypush.message import JSONAble, Message
|
||||
|
||||
|
@ -170,7 +175,10 @@ if 'entity' not in Base.metadata:
|
|||
return normalized_name
|
||||
except ObjectDeletedError as e:
|
||||
logger.warning(
|
||||
f'Could not access column "{col.key}" for entity ID "{self.id}": {e}'
|
||||
'Could not access column "%s" for entity ID "{%s}": {%s}',
|
||||
col.key,
|
||||
self.id,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
|
@ -267,9 +275,9 @@ def _discover_entity_types():
|
|||
isinstance(e, (ImportError, ModuleNotFoundError))
|
||||
and modname[len(__package__) + 1 :] in _import_error_ignored_modules
|
||||
):
|
||||
logger.debug(f'Could not import module {modname}')
|
||||
logger.debug('Could not import module %s', modname)
|
||||
else:
|
||||
logger.warning(f'Could not import module {modname}')
|
||||
logger.warning('Could not import module %s', modname)
|
||||
logger.exception(e)
|
||||
|
||||
continue
|
||||
|
@ -292,7 +300,31 @@ def init_entities_db():
|
|||
"""
|
||||
from platypush.context import get_plugin
|
||||
|
||||
run_db_migrations()
|
||||
_discover_entity_types()
|
||||
db = get_plugin('db')
|
||||
assert db
|
||||
db.create_all(db.get_engine(), Base)
|
||||
|
||||
|
||||
def run_db_migrations():
|
||||
"""
|
||||
Run the database migrations upon engine initialization.
|
||||
"""
|
||||
logger.info('Running database migrations')
|
||||
alembic_ini = os.path.join(
|
||||
os.path.dirname(inspect.getabsfile(platypush)), 'migrations', 'alembic.ini'
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
'-m',
|
||||
'alembic',
|
||||
'-c',
|
||||
alembic_ini,
|
||||
'upgrade',
|
||||
'head',
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
|
29
platypush/entities/variables.py
Normal file
29
platypush/entities/variables.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import logging
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
|
||||
from platypush.common.db import Base
|
||||
|
||||
from . import Entity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if 'variable' not in Base.metadata:
|
||||
|
||||
class Variable(Entity):
|
||||
"""
|
||||
Models a variable entity.
|
||||
"""
|
||||
|
||||
__tablename__ = 'variable'
|
||||
|
||||
id = Column(
|
||||
Integer, ForeignKey('entity.id', ondelete='CASCADE'), primary_key=True
|
||||
)
|
||||
value = Column(String)
|
||||
|
||||
__table_args__ = {'keep_existing': True}
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
}
|
106
platypush/migrations/alembic.ini
Normal file
106
platypush/migrations/alembic.ini
Normal file
|
@ -0,0 +1,106 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
version_locations = %(here)s/alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
version_path_separator = os
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = %(DB_ENGINE)s
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
92
platypush/migrations/alembic/env.py
Normal file
92
platypush/migrations/alembic/env.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from platypush.config import Config
|
||||
from platypush.common.db import Base
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def set_db_engine():
|
||||
db_conf = Config.get('db')
|
||||
assert db_conf, 'Could not retrieve the database configuration'
|
||||
engine = db_conf['engine']
|
||||
assert engine, 'No database engine configured'
|
||||
|
||||
config = context.config
|
||||
section = config.config_ini_section
|
||||
config.set_section_option(section, 'DB_ENGINE', engine)
|
||||
|
||||
|
||||
set_db_engine()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
platypush/migrations/alembic/script.py.mako
Normal file
24
platypush/migrations/alembic/script.py.mako
Normal file
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,153 @@
|
|||
"""Migrate variable table
|
||||
|
||||
Revision ID: c39ac404119b
|
||||
Revises: d030953a871d
|
||||
Create Date: 2023-04-28 22:35:28.307954
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from platypush.entities import Entity
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c39ac404119b'
|
||||
down_revision = 'd030953a871d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Get the connection and the existing `variable` table
|
||||
conn = op.get_bind()
|
||||
metadata = sa.MetaData()
|
||||
metadata.reflect(bind=conn)
|
||||
VariableOld = metadata.tables.get('variable')
|
||||
|
||||
if VariableOld is None:
|
||||
print('The table `variable` does not exist, skipping migration')
|
||||
return
|
||||
|
||||
# Create the `variable_new` table
|
||||
VariableNew = op.create_table(
|
||||
'variable_new',
|
||||
sa.Column(
|
||||
'id',
|
||||
sa.Integer,
|
||||
sa.ForeignKey(Entity.id, ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
),
|
||||
sa.Column('value', sa.String),
|
||||
)
|
||||
|
||||
assert VariableNew is not None, 'Could not create table "variable_new"'
|
||||
|
||||
# Select all existing variables
|
||||
existing_vars = {
|
||||
var.name: var.value for var in conn.execute(sa.select(VariableOld)).all()
|
||||
}
|
||||
|
||||
# Insert all the existing variables as entities
|
||||
if existing_vars:
|
||||
conn.execute(
|
||||
sa.insert(Entity).values(
|
||||
[
|
||||
{
|
||||
'external_id': name,
|
||||
'name': name,
|
||||
'type': 'variable',
|
||||
'plugin': 'variable',
|
||||
}
|
||||
for name in existing_vars
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Fetch all the newly inserted variables
|
||||
new_vars = {
|
||||
entity.id: entity.name
|
||||
for entity in conn.execute(
|
||||
sa.select(Entity.id, Entity.name).where(
|
||||
sa.or_(
|
||||
*[
|
||||
sa.and_(
|
||||
Entity.external_id == name,
|
||||
Entity.type == 'variable',
|
||||
Entity.plugin == 'variable',
|
||||
)
|
||||
for name in existing_vars
|
||||
]
|
||||
)
|
||||
)
|
||||
).all()
|
||||
}
|
||||
|
||||
# Insert the mapping on the `variable_new` table
|
||||
op.bulk_insert(
|
||||
VariableNew,
|
||||
[
|
||||
{
|
||||
'id': id,
|
||||
'value': existing_vars.get(name),
|
||||
}
|
||||
for id, name in new_vars.items()
|
||||
],
|
||||
)
|
||||
|
||||
# Rename/drop the tables
|
||||
op.rename_table('variable', 'variable_old')
|
||||
op.rename_table('variable_new', 'variable')
|
||||
op.drop_table('variable_old')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Get the connection and the existing `variable` table
|
||||
conn = op.get_bind()
|
||||
metadata = sa.MetaData()
|
||||
metadata.reflect(bind=conn)
|
||||
VariableNew = metadata.tables['variable']
|
||||
|
||||
if VariableNew is None:
|
||||
print('The table `variable` does not exist, skipping migration')
|
||||
return
|
||||
|
||||
# Create the `variable_old` table
|
||||
VariableOld = op.create_table(
|
||||
'variable_old',
|
||||
sa.Column('name', sa.String, primary_key=True, nullable=False),
|
||||
sa.Column('value', sa.String),
|
||||
)
|
||||
|
||||
assert VariableOld is not None, 'Could not create table "variable_old"'
|
||||
|
||||
# Select all existing variables
|
||||
existing_vars = {
|
||||
var.name: var.value
|
||||
for var in conn.execute(
|
||||
sa.select(Entity.name, VariableNew.c.value).join(
|
||||
Entity, Entity.id == VariableNew.c.id
|
||||
)
|
||||
).all()
|
||||
}
|
||||
|
||||
# Insert the mapping on the `variable_old` table
|
||||
if existing_vars:
|
||||
op.bulk_insert(
|
||||
VariableOld,
|
||||
[
|
||||
{
|
||||
'name': name,
|
||||
'value': value,
|
||||
}
|
||||
for name, value in existing_vars.items()
|
||||
],
|
||||
)
|
||||
|
||||
# Delete existing references on the `entity` table
|
||||
conn.execute(sa.delete(Entity).where(Entity.type == 'variable'))
|
||||
|
||||
# Rename/drop the tables
|
||||
op.rename_table('variable', 'variable_new')
|
||||
op.rename_table('variable_old', 'variable')
|
||||
op.drop_table('variable_new')
|
|
@ -0,0 +1,22 @@
|
|||
"""Base alembic version
|
||||
|
||||
Revision ID: d030953a871d
|
||||
Revises:
|
||||
Create Date: 2023-04-28 22:32:49.460118
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd030953a871d'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
|
@ -67,6 +67,42 @@ class Plugin(EventGenerator, ExtensionWithManifest): # lgtm [py/missing-call-to
|
|||
get_decorators(self.__class__, climb_class_hierarchy=True).get('action', [])
|
||||
)
|
||||
|
||||
@property
|
||||
def _db(self):
|
||||
"""
|
||||
:return: The reference to the :class:`platypush.plugins.db.DbPlugin`.
|
||||
"""
|
||||
from platypush.context import get_plugin
|
||||
from platypush.plugins.db import DbPlugin
|
||||
|
||||
db: DbPlugin = get_plugin(DbPlugin) # type: ignore
|
||||
assert db, 'db plugin not initialized'
|
||||
return db
|
||||
|
||||
@property
|
||||
def _redis(self):
|
||||
"""
|
||||
:return: The reference to the :class:`platypush.plugins.redis.RedisPlugin`.
|
||||
"""
|
||||
from platypush.context import get_plugin
|
||||
from platypush.plugins.redis import RedisPlugin
|
||||
|
||||
redis: RedisPlugin = get_plugin(RedisPlugin) # type: ignore
|
||||
assert redis, 'db plugin not initialized'
|
||||
return redis
|
||||
|
||||
@property
|
||||
def _entities(self):
|
||||
"""
|
||||
:return: The reference to the :class:`platypush.plugins.entities.EntitiesPlugin`.
|
||||
"""
|
||||
from platypush.context import get_plugin
|
||||
from platypush.plugins.entities import EntitiesPlugin
|
||||
|
||||
entities: EntitiesPlugin = get_plugin(EntitiesPlugin) # type: ignore
|
||||
assert entities, 'entities plugin not initialized'
|
||||
return entities
|
||||
|
||||
def run(self, method, *args, **kwargs):
|
||||
assert (
|
||||
method in self.registered_actions
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from queue import Queue, Empty
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from traceback import format_exception
|
||||
from typing import Optional, Any, Collection, Mapping
|
||||
|
||||
from sqlalchemy import or_, text
|
||||
|
@ -49,7 +50,9 @@ class EntitiesPlugin(Plugin):
|
|||
"""
|
||||
entity_registry = get_entities_registry()
|
||||
selected_types = []
|
||||
all_types = {e.__tablename__.lower(): e for e in entity_registry}
|
||||
all_types = {
|
||||
e.__tablename__.lower(): e for e in entity_registry # type: ignore
|
||||
}
|
||||
|
||||
if types:
|
||||
selected_types = {t.lower() for t in types}
|
||||
|
@ -148,8 +151,9 @@ class EntitiesPlugin(Plugin):
|
|||
plugin_name, result = q.get(block=True, timeout=0.5)
|
||||
if isinstance(result, Exception):
|
||||
self.logger.warning(
|
||||
f'Could not load results from plugin {plugin_name}: {result}'
|
||||
'Could not load results from plugin %s: %s', plugin_name, result
|
||||
)
|
||||
self.logger.warning(''.join(format_exception(result)))
|
||||
else:
|
||||
results.append(result)
|
||||
except Empty:
|
||||
|
@ -243,7 +247,10 @@ class EntitiesPlugin(Plugin):
|
|||
with self._get_session(locked=True) as session:
|
||||
objs = session.query(Entity).filter(Entity.id.in_(entities.keys())).all()
|
||||
for obj in objs:
|
||||
obj.meta = {**(obj.meta or {}), **(entities.get(str(obj.id), {}))}
|
||||
obj.meta = { # type: ignore
|
||||
**dict(obj.meta or {}), # type: ignore
|
||||
**(entities.get(str(obj.id), {})),
|
||||
}
|
||||
session.add(obj)
|
||||
|
||||
session.commit()
|
||||
|
|
|
@ -606,6 +606,7 @@ class SystemPlugin(SensorPlugin, EntityManager):
|
|||
**disk,
|
||||
)
|
||||
for disk in entities['disks']
|
||||
if disk.get('device')
|
||||
],
|
||||
*[
|
||||
NetworkInterfaceModel(
|
||||
|
@ -615,6 +616,7 @@ class SystemPlugin(SensorPlugin, EntityManager):
|
|||
**nic,
|
||||
)
|
||||
for nic in entities.get('network', [])
|
||||
if nic.get('interface')
|
||||
],
|
||||
*[
|
||||
SystemTemperature(
|
||||
|
@ -624,6 +626,7 @@ class SystemPlugin(SensorPlugin, EntityManager):
|
|||
**temp,
|
||||
)
|
||||
for temp in entities.get('temperature', [])
|
||||
if temp.get('id') and temp.get('label')
|
||||
],
|
||||
*[
|
||||
SystemFan(
|
||||
|
@ -633,6 +636,7 @@ class SystemPlugin(SensorPlugin, EntityManager):
|
|||
**fan,
|
||||
)
|
||||
for fan in entities.get('fans', [])
|
||||
if fan.get('id') and fan.get('label')
|
||||
],
|
||||
*[
|
||||
SystemBattery(
|
||||
|
|
|
@ -1,24 +1,12 @@
|
|||
from sqlalchemy import Column, String
|
||||
from typing import Collection, Dict, Iterable, Optional, Union
|
||||
from typing_extensions import override
|
||||
|
||||
from platypush.common.db import declarative_base
|
||||
from platypush.context import get_plugin
|
||||
from platypush.entities import EntityManager
|
||||
from platypush.entities.variables import Variable
|
||||
from platypush.plugins import Plugin, action
|
||||
from platypush.plugins.db import DbPlugin
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Variable(Base):
|
||||
"""Models the variable table"""
|
||||
|
||||
__tablename__ = 'variable'
|
||||
|
||||
name = Column(String, primary_key=True, nullable=False)
|
||||
value = Column(String)
|
||||
|
||||
|
||||
class VariablePlugin(Plugin):
|
||||
class VariablePlugin(Plugin, EntityManager):
|
||||
"""
|
||||
This plugin allows you to manipulate context variables that can be
|
||||
accessed across your tasks. It requires the :mod:`platypush.plugins.db`
|
||||
|
@ -27,39 +15,34 @@ class VariablePlugin(Plugin):
|
|||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
The plugin will create a table named ``variable`` on the database
|
||||
configured in the :mod:`platypush.plugins.db` plugin. You'll have
|
||||
to specify a default ``engine`` in your ``db`` plugin configuration.
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
db_plugin = get_plugin('db')
|
||||
redis_plugin = get_plugin('redis')
|
||||
assert db_plugin, 'Database plugin not configured'
|
||||
assert redis_plugin, 'Redis plugin not configured'
|
||||
|
||||
self.redis_plugin = redis_plugin
|
||||
self.db_plugin: DbPlugin = db_plugin
|
||||
self.db_plugin.create_all(self.db_plugin.get_engine(), Base)
|
||||
db = self._db
|
||||
self._db_vars: Dict[str, Optional[str]] = {}
|
||||
""" Local cache for db variables. """
|
||||
|
||||
with db.get_session() as session:
|
||||
self._db_vars.update(
|
||||
{ # type: ignore
|
||||
str(var.name): var.value for var in session.query(Variable).all()
|
||||
}
|
||||
)
|
||||
|
||||
@action
|
||||
def get(self, name, default_value=None):
|
||||
def get(self, name: Optional[str] = None, default_value=None):
|
||||
"""
|
||||
Get the value of a variable by name from the local db.
|
||||
|
||||
:param name: Variable name
|
||||
:type name: str
|
||||
|
||||
:param name: Variable name. If not specified, all the stored variables will be returned.
|
||||
:param default_value: What will be returned if the variable is not defined (default: None)
|
||||
|
||||
:returns: A map in the format ``{"<name>":"<value>"}``
|
||||
"""
|
||||
|
||||
with self.db_plugin.get_session() as session:
|
||||
var = session.query(Variable).filter_by(name=name).first()
|
||||
|
||||
return {name: (var.value if var is not None else default_value)}
|
||||
return (
|
||||
{name: self._db_vars.get(name, default_value)}
|
||||
if name is not None
|
||||
else self.status().output
|
||||
)
|
||||
|
||||
@action
|
||||
def set(self, **kwargs):
|
||||
|
@ -69,53 +52,36 @@ class VariablePlugin(Plugin):
|
|||
:param kwargs: Key-value list of variables to set (e.g. ``foo='bar', answer=42``)
|
||||
"""
|
||||
|
||||
with self.db_plugin.get_session() as session:
|
||||
existing_vars = {
|
||||
var.name: var
|
||||
for var in session.query(Variable)
|
||||
.filter(Variable.name.in_(kwargs.keys()))
|
||||
.all()
|
||||
}
|
||||
|
||||
new_vars = {
|
||||
name: Variable(name=name, value=value)
|
||||
for name, value in kwargs.items()
|
||||
if name not in existing_vars
|
||||
}
|
||||
|
||||
for name, var in existing_vars.items():
|
||||
var.value = kwargs[name] # type: ignore
|
||||
|
||||
session.add_all([*existing_vars.values(), *new_vars.values()])
|
||||
|
||||
self.publish_entities(kwargs)
|
||||
self._db_vars.update(kwargs)
|
||||
return kwargs
|
||||
|
||||
@action
|
||||
def unset(self, name):
|
||||
def unset(self, name: str):
|
||||
"""
|
||||
Unset a variable by name if it's set on the local db.
|
||||
|
||||
:param name: Name of the variable to remove
|
||||
:type name: str
|
||||
:param name: Name of the variable to remove.
|
||||
"""
|
||||
|
||||
with self.db_plugin.get_session() as session:
|
||||
session.query(Variable).filter_by(name=name).delete()
|
||||
with self._db.get_session() as session:
|
||||
entity = session.query(Variable).filter(Variable.name == name).first()
|
||||
if entity is not None:
|
||||
self._entities.delete(entity.id)
|
||||
|
||||
self._db_vars.pop(name, None)
|
||||
return True
|
||||
|
||||
@action
|
||||
def mget(self, name):
|
||||
def mget(self, name: str):
|
||||
"""
|
||||
Get the value of a variable by name from Redis.
|
||||
|
||||
:param name: Variable name
|
||||
:type name: str
|
||||
|
||||
:returns: A map in the format ``{"<name>":"<value>"}``
|
||||
"""
|
||||
|
||||
return self.redis_plugin.mget([name])
|
||||
return self._redis.mget([name])
|
||||
|
||||
@action
|
||||
def mset(self, **kwargs):
|
||||
|
@ -123,37 +89,65 @@ class VariablePlugin(Plugin):
|
|||
Set a variable or a set of variables on Redis.
|
||||
|
||||
:param kwargs: Key-value list of variables to set (e.g. ``foo='bar', answer=42``)
|
||||
|
||||
:returns: A map with the set variables
|
||||
"""
|
||||
|
||||
self.redis_plugin.mset(**kwargs)
|
||||
self._redis.mset(**kwargs)
|
||||
return kwargs
|
||||
|
||||
@action
|
||||
def munset(self, name):
|
||||
def munset(self, name: str):
|
||||
"""
|
||||
Unset a Redis variable by name if it's set
|
||||
|
||||
:param name: Name of the variable to remove
|
||||
:type name: str
|
||||
"""
|
||||
|
||||
return self.redis_plugin.delete(name)
|
||||
return self._redis.delete(name)
|
||||
|
||||
@action
|
||||
def expire(self, name, expire):
|
||||
def expire(self, name: str, expire: int):
|
||||
"""
|
||||
Set a variable expiration on Redis
|
||||
|
||||
:param name: Variable name
|
||||
:type name: str
|
||||
|
||||
:param expire: Expiration time in seconds
|
||||
:type expire: int
|
||||
"""
|
||||
|
||||
return self.redis_plugin.expire(name, expire)
|
||||
return self._redis.expire(name, expire)
|
||||
|
||||
@override
|
||||
def transform_entities(
|
||||
self, entities: Union[dict, Iterable]
|
||||
) -> Collection[Variable]:
|
||||
variables = (
|
||||
[
|
||||
{
|
||||
'name': name,
|
||||
'value': value,
|
||||
}
|
||||
for name, value in entities.items()
|
||||
]
|
||||
if isinstance(entities, dict)
|
||||
else entities
|
||||
)
|
||||
|
||||
return super().transform_entities(
|
||||
[
|
||||
Variable(id=var['name'], name=var['name'], value=var['value'])
|
||||
for var in variables
|
||||
]
|
||||
)
|
||||
|
||||
@override
|
||||
@action
|
||||
def status(self, *_, **__):
|
||||
variables = {
|
||||
name: value for name, value in self._db_vars.items() if value is not None
|
||||
}
|
||||
|
||||
self.publish_entities(variables)
|
||||
return variables
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -13,7 +13,8 @@ python-dateutil
|
|||
tz
|
||||
frozendict
|
||||
requests
|
||||
sqlalchemy<2.0.0
|
||||
sqlalchemy
|
||||
alembic
|
||||
bcrypt
|
||||
rsa
|
||||
zeroconf>=0.27.0
|
||||
|
|
4
setup.py
4
setup.py
|
@ -38,6 +38,9 @@ setup(
|
|||
url="https://platypush.tech",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
package_data={
|
||||
'platypush': ['alembic.ini', 'alembic/*', 'alembic/**/*'],
|
||||
},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'platypush=platypush:main',
|
||||
|
@ -58,6 +61,7 @@ setup(
|
|||
'requests',
|
||||
'croniter',
|
||||
'sqlalchemy',
|
||||
'alembic',
|
||||
'websockets',
|
||||
'websocket-client',
|
||||
'wheel',
|
||||
|
|
Loading…
Reference in a new issue