Compare commits
11 commits
2411b961e8
...
12096f2dbe
Author | SHA1 | Date | |
---|---|---|---|
12096f2dbe | |||
40f81b105f | |||
9d66b63266 | |||
6e9263c4e4 | |||
b568876474 | |||
aa04741daa | |||
f74fab795d | |||
243de15813 | |||
256d9adbf2 | |||
4144e4f842 | |||
878fe91155 |
37 changed files with 328 additions and 156 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.484f9c7c.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.36cc00f9.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.da4780e5.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.c91c6b3d.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/3077.bc5c0923.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/3077.bc5c0923.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/213-legacy.f5ffee0f.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/213-legacy.f5ffee0f.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/213-legacy.f5ffee0f.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/213-legacy.f5ffee0f.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/213.f2e3adcf.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/213.f2e3adcf.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/js/213.f2e3adcf.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/213.f2e3adcf.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
2
platypush/backend/http/webapp/dist/static/js/3077-legacy.f26a945c.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/3077-legacy.f26a945c.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[3077,3459],{6:function(e,t,n){n.d(t,{Z:function(){return f}});var o=n(6252),i=n(3577),r=n(9963),l=function(e){return(0,o.dD)("data-v-a6396ae8"),e=e(),(0,o.Cn)(),e},c=["checked"],a=l((function(){return(0,o._)("div",{class:"switch"},[(0,o._)("div",{class:"dot"})],-1)})),u={class:"label"};function s(e,t,n,l,s,d){return(0,o.wg)(),(0,o.iD)("div",{class:(0,i.C_)(["power-switch",{disabled:n.disabled}]),onClick:t[0]||(t[0]=(0,r.iM)((function(){return d.onInput&&d.onInput.apply(d,arguments)}),["stop"]))},[(0,o._)("input",{type:"checkbox",checked:n.value},null,8,c),(0,o._)("label",null,[a,(0,o._)("span",u,[(0,o.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)}}},p=n(3744);const v=(0,p.Z)(d,[["render",s],["__scopeId","data-v-a6396ae8"]]);var f=v},3077:function(e,t,n){n.r(t),n.d(t,{default:function(){return b}});n(8309);var o=n(6252),i=n(3577),r=n(9963),l={class:"entity bluetooth-service-container"},c={class:"head"},a={class:"col-1 icon"},u={class:"col-9 label"},s=["textContent"],d={class:"col-2 connector pull-right"};function p(e,t,n,p,v,f){var h=(0,o.up)("EntityIcon"),m=(0,o.up)("ToggleSwitch");return(0,o.wg)(),(0,o.iD)("div",l,[(0,o._)("div",c,[(0,o._)("div",a,[(0,o.Wm)(h,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,o._)("div",u,[(0,o._)("div",{class:"name",textContent:(0,i.zw)(e.value.name)},null,8,s)]),(0,o._)("div",d,[(0,o.Wm)(m,{value:e.parent.connected,disabled:e.loading,onInput:f.connect,onClick:t[0]||(t[0]=(0,r.iM)((function(){}),["stop"]))},null,8,["value","disabled","onInput"])])])])}var v=n(8534),f=(n(5666),n(6)),h=n(3459),m=n(7909),g={name:"BluetoothService",components:{ToggleSwitch:f.Z,EntityIcon:h["default"]},mixins:[m["default"]],methods:{connect:function(e){var t=this;return(0,v.Z)(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.stopPropagation(),t.$emit("loading",!0),n.prev=2,n.next=5,t.request("bluetooth.connect",{device:t.parent.address,service_uuid:t.uuid});case 5:return n.prev=5,t.$emit("loading",!1),n.finish(5);case 8:case"end":return n.stop()}}),n,null,[[2,,5,8]])})))()},disconnect:function(e){var t=this;return(0,v.Z)(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.stopPropagation(),t.$emit("loading",!0),n.prev=2,n.next=5,t.request("bluetooth.disconnect",{device:t.parent.address});case 5:return n.prev=5,t.$emit("loading",!1),n.finish(5);case 8:case"end":return n.stop()}}),n,null,[[2,,5,8]])})))()}}},y=n(3744);const w=(0,y.Z)(g,[["render",p],["__scopeId","data-v-a94a2cfa"]]);var b=w},3459:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var o=n(6252),i=n(3577),r=n(3540),l={key:0,src:r,class:"loading"},c={key:1,class:"fas fa-circle-exclamation error"};function a(e,t,n,r,a,u){var s=(0,o.up)("Icon");return(0,o.wg)(),(0,o.iD)("div",{class:(0,i.C_)(["entity-icon-container",{"with-color-fill":!!u.colorFill}]),style:(0,i.j5)(u.colorFillStyle)},[n.loading?((0,o.wg)(),(0,o.iD)("img",l)):n.error?((0,o.wg)(),(0,o.iD)("i",c)):((0,o.wg)(),(0,o.j4)(s,(0,i.vs)((0,o.dG)({key:2},u.computedIconNormalized)),null,16))],6)}var u=n(4648),s=(n(7941),n(7042),n(1478)),d={name:"EntityIcon",components:{Icon:s.Z},props:{loading:{type:Boolean,default:!1},error:{type:Boolean,default:!1},entity:{type:Object,required:!0},icon:{type:Object,default:function(){}},hasColorFill:{type:Boolean,default:!1}},data:function(){return{component:null,modalVisible:!1}},computed:{computedIcon:function(){var e,t,n=(0,u.Z)({},(null===(e=this.entity)||void 0===e||null===(t=e.meta)||void 0===t?void 0:t.icon)||{});return Object.keys(this.icon||{}).length&&(n=this.icon),(0,u.Z)({},n)},colorFill:function(){return this.hasColorFill&&this.computedIcon.color},colorFillStyle:function(){return this.colorFill&&!this.error?{background:this.colorFill}:{}},computedIconNormalized:function(){var e=(0,u.Z)({},this.computedIcon);return this.colorFill&&delete e.color,e},type:function(){var e=this.entity.type||"";return e.charAt(0).toUpperCase()+e.slice(1)}}},p=n(3744);const v=(0,p.Z)(d,[["render",a],["__scopeId","data-v-4fad24e6"]]);var f=v},3540:function(e,t,n){e.exports=n.p+"static/img/spinner.c0bee445.gif"}}]);
|
||||||
|
//# sourceMappingURL=3077-legacy.f26a945c.js.map
|
1
platypush/backend/http/webapp/dist/static/js/3077-legacy.f26a945c.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/3077-legacy.f26a945c.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/webapp/dist/static/js/3077.af4019ef.js
vendored
Normal file
2
platypush/backend/http/webapp/dist/static/js/3077.af4019ef.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[3077,3459],{6:function(t,e,n){n.d(e,{Z:function(){return v}});var o=n(6252),i=n(3577),l=n(9963);const a=t=>((0,o.dD)("data-v-a6396ae8"),t=t(),(0,o.Cn)(),t),c=["checked"],s=a((()=>(0,o._)("div",{class:"switch"},[(0,o._)("div",{class:"dot"})],-1))),r={class:"label"};function d(t,e,n,a,d,u){return(0,o.wg)(),(0,o.iD)("div",{class:(0,i.C_)(["power-switch",{disabled:n.disabled}]),onClick:e[0]||(e[0]=(0,l.iM)(((...t)=>u.onInput&&u.onInput(...t)),["stop"]))},[(0,o._)("input",{type:"checkbox",checked:n.value},null,8,c),(0,o._)("label",null,[s,(0,o._)("span",r,[(0,o.WI)(t.$slots,"default",{},void 0,!0)])])],2)}var u={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(t){if(this.disabled)return!1;this.$emit("input",t)}}},p=n(3744);const h=(0,p.Z)(u,[["render",d],["__scopeId","data-v-a6396ae8"]]);var v=h},3077:function(t,e,n){n.r(e),n.d(e,{default:function(){return b}});var o=n(6252),i=n(3577),l=n(9963);const a={class:"entity bluetooth-service-container"},c={class:"head"},s={class:"col-1 icon"},r={class:"col-9 label"},d=["textContent"],u={class:"col-2 connector pull-right"};function p(t,e,n,p,h,v){const y=(0,o.up)("EntityIcon"),f=(0,o.up)("ToggleSwitch");return(0,o.wg)(),(0,o.iD)("div",a,[(0,o._)("div",c,[(0,o._)("div",s,[(0,o.Wm)(y,{entity:t.value,loading:t.loading,error:t.error},null,8,["entity","loading","error"])]),(0,o._)("div",r,[(0,o._)("div",{class:"name",textContent:(0,i.zw)(t.value.name)},null,8,d)]),(0,o._)("div",u,[(0,o.Wm)(f,{value:t.parent.connected,disabled:t.loading,onInput:v.connect,onClick:e[0]||(e[0]=(0,l.iM)((()=>{}),["stop"]))},null,8,["value","disabled","onInput"])])])])}var h=n(6),v=n(3459),y=n(7909),f={name:"BluetoothService",components:{ToggleSwitch:h.Z,EntityIcon:v["default"]},mixins:[y["default"]],methods:{async connect(t){t.stopPropagation(),this.$emit("loading",!0);try{await this.request("bluetooth.connect",{device:this.parent.address,service_uuid:this.uuid})}finally{this.$emit("loading",!1)}},async disconnect(t){t.stopPropagation(),this.$emit("loading",!0);try{await this.request("bluetooth.disconnect",{device:this.parent.address})}finally{this.$emit("loading",!1)}}}},m=n(3744);const g=(0,m.Z)(f,[["render",p],["__scopeId","data-v-a94a2cfa"]]);var b=g},3459:function(t,e,n){n.r(e),n.d(e,{default:function(){return h}});var o=n(6252),i=n(3577),l=n(3540);const a={key:0,src:l,class:"loading"},c={key:1,class:"fas fa-circle-exclamation error"};function s(t,e,n,l,s,r){const d=(0,o.up)("Icon");return(0,o.wg)(),(0,o.iD)("div",{class:(0,i.C_)(["entity-icon-container",{"with-color-fill":!!r.colorFill}]),style:(0,i.j5)(r.colorFillStyle)},[n.loading?((0,o.wg)(),(0,o.iD)("img",a)):n.error?((0,o.wg)(),(0,o.iD)("i",c)):((0,o.wg)(),(0,o.j4)(d,(0,i.vs)((0,o.dG)({key:2},r.computedIconNormalized)),null,16))],6)}var r=n(1478),d={name:"EntityIcon",components:{Icon:r.Z},props:{loading:{type:Boolean,default:!1},error:{type:Boolean,default:!1},entity:{type:Object,required:!0},icon:{type:Object,default:()=>{}},hasColorFill:{type:Boolean,default:!1}},data(){return{component:null,modalVisible:!1}},computed:{computedIcon(){let t={...this.entity?.meta?.icon||{}};return Object.keys(this.icon||{}).length&&(t=this.icon),{...t}},colorFill(){return this.hasColorFill&&this.computedIcon.color},colorFillStyle(){return this.colorFill&&!this.error?{background:this.colorFill}:{}},computedIconNormalized(){const t={...this.computedIcon};return this.colorFill&&delete t.color,t},type(){let t=this.entity.type||"";return t.charAt(0).toUpperCase()+t.slice(1)}}},u=n(3744);const p=(0,u.Z)(d,[["render",s],["__scopeId","data-v-4fad24e6"]]);var h=p},3540:function(t,e,n){t.exports=n.p+"static/img/spinner.c0bee445.gif"}}]);
|
||||||
|
//# sourceMappingURL=3077.af4019ef.js.map
|
1
platypush/backend/http/webapp/dist/static/js/3077.af4019ef.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/3077.af4019ef.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-legacy.c91c6b3d.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/app-legacy.c91c6b3d.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.da4780e5.js.map
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/js/app.da4780e5.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"bluetooth": {
|
||||||
|
"class": "fab fa-bluetooth"
|
||||||
|
},
|
||||||
"camera.android.ipcam": {
|
"camera.android.ipcam": {
|
||||||
"class": "fab fa-android"
|
"class": "fab fa-android"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity bluetooth-service-container">
|
||||||
|
<div class="head">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<EntityIcon
|
||||||
|
:entity="value"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-9 label">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-2 connector pull-right">
|
||||||
|
<ToggleSwitch
|
||||||
|
:value="parent.connected"
|
||||||
|
:disabled="loading"
|
||||||
|
@input="connect"
|
||||||
|
@click.stop />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ToggleSwitch from "@/components/elements/ToggleSwitch"
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BluetoothService',
|
||||||
|
components: {ToggleSwitch, EntityIcon},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async connect(event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.$emit('loading', true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.request('bluetooth.connect', {
|
||||||
|
device: this.parent.address,
|
||||||
|
service_uuid: this.uuid,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.$emit('loading', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async disconnect(event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.$emit('loading', true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.request('bluetooth.disconnect', {
|
||||||
|
device: this.parent.address,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.$emit('loading', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
.switch {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,6 +7,7 @@
|
||||||
<component
|
<component
|
||||||
:is="component"
|
:is="component"
|
||||||
:value="value"
|
:value="value"
|
||||||
|
:parent="parent"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
ref="instance"
|
ref="instance"
|
||||||
:error="error || value?.reachable == false"
|
:error="error || value?.reachable == false"
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
<div class="child" v-for="entity in computedChildren" :key="entity.id">
|
<div class="child" v-for="entity in computedChildren" :key="entity.id">
|
||||||
<Entity
|
<Entity
|
||||||
:value="entity"
|
:value="entity"
|
||||||
|
:parent="value"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:level="level + 1"
|
:level="level + 1"
|
||||||
@input="$emit('input', entity)" />
|
@input="$emit('input', entity)" />
|
||||||
|
|
|
@ -21,6 +21,11 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parent: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
|
||||||
children: {
|
children: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
|
|
@ -39,6 +39,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"bluetooth_service": {
|
||||||
|
"name": "Service",
|
||||||
|
"name_plural": "Services",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-satellite-dish"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"device": {
|
"device": {
|
||||||
"name": "Device",
|
"name": "Device",
|
||||||
"name_plural": "Devices",
|
"name_plural": "Devices",
|
||||||
|
|
|
@ -5,6 +5,7 @@ from typing import Collection, Optional
|
||||||
|
|
||||||
from ._base import (
|
from ._base import (
|
||||||
Entity,
|
Entity,
|
||||||
|
EntityKey,
|
||||||
EntitySavedCallback,
|
EntitySavedCallback,
|
||||||
get_entities_registry,
|
get_entities_registry,
|
||||||
init_entities_db,
|
init_entities_db,
|
||||||
|
@ -80,6 +81,7 @@ __all__ = (
|
||||||
'DimmerEntityManager',
|
'DimmerEntityManager',
|
||||||
'EntitiesEngine',
|
'EntitiesEngine',
|
||||||
'Entity',
|
'Entity',
|
||||||
|
'EntityKey',
|
||||||
'EntityManager',
|
'EntityManager',
|
||||||
'EntitySavedCallback',
|
'EntitySavedCallback',
|
||||||
'EnumSwitchEntityManager',
|
'EnumSwitchEntityManager',
|
||||||
|
|
|
@ -27,6 +27,11 @@ from platypush.message import JSONAble
|
||||||
EntityRegistryType = Dict[str, Type['Entity']]
|
EntityRegistryType = Dict[str, Type['Entity']]
|
||||||
entities_registry: EntityRegistryType = {}
|
entities_registry: EntityRegistryType = {}
|
||||||
|
|
||||||
|
EntityKey = Tuple[str, str]
|
||||||
|
""" The entity's logical key, as an ``<external_id, plugin>`` tuple. """
|
||||||
|
EntityMapping = Dict[EntityKey, 'Entity']
|
||||||
|
""" Internal mapping for entities used for deduplication/merge/upsert. """
|
||||||
|
|
||||||
_import_error_ignored_modules: Final[Set[str]] = {'bluetooth'}
|
_import_error_ignored_modules: Final[Set[str]] = {'bluetooth'}
|
||||||
"""
|
"""
|
||||||
ImportError exceptions will be ignored for these entity submodules when
|
ImportError exceptions will be ignored for these entity submodules when
|
||||||
|
@ -110,7 +115,7 @@ if 'entity' not in Base.metadata:
|
||||||
return tuple(inspector.mapper.column_attrs)
|
return tuple(inspector.mapper.column_attrs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_key(self) -> Tuple[str, str]:
|
def entity_key(self) -> EntityKey:
|
||||||
"""
|
"""
|
||||||
This method returns the "external" key of an entity.
|
This method returns the "external" key of an entity.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.entities import Entity
|
from platypush.entities import Entity
|
||||||
from platypush.message.event.entities import EntityUpdateEvent
|
from platypush.message.event.entities import EntityUpdateEvent
|
||||||
from platypush.utils import set_thread_name
|
from platypush.utils import set_thread_name
|
||||||
|
|
||||||
from platypush.entities._base import EntitySavedCallback
|
from platypush.entities._base import EntityKey, EntitySavedCallback
|
||||||
from platypush.entities._engine.queue import EntitiesQueue
|
from platypush.entities._engine.queue import EntitiesQueue
|
||||||
from platypush.entities._engine.repo import EntitiesRepository
|
from platypush.entities._engine.repo import EntitiesRepository
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class EntitiesEngine(Thread):
|
||||||
""" Queue where all entity upsert requests are received."""
|
""" Queue where all entity upsert requests are received."""
|
||||||
self._repo = EntitiesRepository()
|
self._repo = EntitiesRepository()
|
||||||
""" The repository of the processed entities. """
|
""" The repository of the processed entities. """
|
||||||
self._callbacks: Dict[Tuple[str, str], EntitySavedCallback] = {}
|
self._callbacks: Dict[EntityKey, EntitySavedCallback] = {}
|
||||||
""" (external_id, plugin) -> callback mapping"""
|
""" (external_id, plugin) -> callback mapping"""
|
||||||
|
|
||||||
def post(self, *entities: Entity, callback: Optional[EntitySavedCallback] = None):
|
def post(self, *entities: Entity, callback: Optional[EntitySavedCallback] = None):
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Iterable, Tuple
|
from typing import Dict, Iterable, Optional, Tuple
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from platypush.entities import Entity
|
from platypush.entities._base import Entity, EntityMapping
|
||||||
|
|
||||||
# pylint: disable=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
from platypush.entities._engine.repo.db import EntitiesDb
|
from platypush.entities._engine.repo.db import EntitiesDb
|
||||||
|
@ -20,7 +20,7 @@ class EntitiesRepository:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._db = EntitiesDb()
|
self._db = EntitiesDb()
|
||||||
self._merger = EntitiesMerger(self)
|
self._merge = EntitiesMerger()
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, session: Session, entities: Iterable[Entity]
|
self, session: Session, entities: Iterable[Entity]
|
||||||
|
@ -43,7 +43,63 @@ class EntitiesRepository:
|
||||||
autocommit=False,
|
autocommit=False,
|
||||||
expire_on_commit=False,
|
expire_on_commit=False,
|
||||||
) as session:
|
) as session:
|
||||||
merged_entities = self._merger.merge(session, entities)
|
merged_entities = self._merge(
|
||||||
|
session,
|
||||||
|
entities,
|
||||||
|
existing_entities=self._fetch_all_and_flatten(session, entities),
|
||||||
|
)
|
||||||
|
|
||||||
merged_entities = self._db.upsert(session, merged_entities)
|
merged_entities = self._db.upsert(session, merged_entities)
|
||||||
|
|
||||||
return merged_entities
|
return merged_entities
|
||||||
|
|
||||||
|
def _fetch_all_and_flatten(
|
||||||
|
self,
|
||||||
|
session: Session,
|
||||||
|
entities: Iterable[Entity],
|
||||||
|
) -> EntityMapping:
|
||||||
|
"""
|
||||||
|
Given a collection of entities, retrieves their persisted instances
|
||||||
|
(lookup is performed by ``entity_key``), and it also recursively
|
||||||
|
expands their relationships, so the session is updated with the latest
|
||||||
|
persisted versions of all the objects in the hierarchy.
|
||||||
|
|
||||||
|
:return: An ``entity_key -> entity`` mapping.
|
||||||
|
"""
|
||||||
|
expanded_entities = {}
|
||||||
|
for entity in entities:
|
||||||
|
root_entity = self._get_root_entity(session, entity)
|
||||||
|
expanded_entities.update(self._expand_children([root_entity]))
|
||||||
|
expanded_entities.update(self._expand_children([entity]))
|
||||||
|
|
||||||
|
return self.get(session, expanded_entities.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _expand_children(
|
||||||
|
cls,
|
||||||
|
entities: Iterable[Entity],
|
||||||
|
all_entities: Optional[EntityMapping] = None,
|
||||||
|
) -> EntityMapping:
|
||||||
|
"""
|
||||||
|
Recursively expands and flattens all the children of a set of entities
|
||||||
|
into an ``entity_key -> entity`` mapping.
|
||||||
|
"""
|
||||||
|
all_entities = all_entities or {}
|
||||||
|
for entity in entities:
|
||||||
|
all_entities[entity.entity_key] = entity
|
||||||
|
cls._expand_children(entity.children, all_entities)
|
||||||
|
|
||||||
|
return all_entities
|
||||||
|
|
||||||
|
def _get_root_entity(self, session: Session, entity: Entity) -> Entity:
|
||||||
|
"""
|
||||||
|
Retrieve the root entity (i.e. the one with a null parent) of an
|
||||||
|
entity.
|
||||||
|
"""
|
||||||
|
parent = entity
|
||||||
|
while parent:
|
||||||
|
parent = self._merge.get_parent(session, entity)
|
||||||
|
if parent:
|
||||||
|
entity = parent
|
||||||
|
|
||||||
|
return entity
|
||||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy import and_, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.entities import Entity
|
from platypush.entities._base import Entity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -1,34 +1,30 @@
|
||||||
from typing import Dict, Iterable, List, Optional, Tuple
|
from typing import Iterable, List, Optional
|
||||||
|
|
||||||
from sqlalchemy.orm import Session, exc
|
from sqlalchemy.orm import Session, exc
|
||||||
|
|
||||||
from platypush.entities import Entity
|
from platypush.entities._base import Entity, EntityMapping
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class EntitiesMerger:
|
class EntitiesMerger:
|
||||||
"""
|
"""
|
||||||
This object is in charge of detecting and merging entities that already
|
A stateless functor in charge of detecting and merging entities that
|
||||||
exist on the database before flushing the session.
|
already exist on the database before flushing the session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, repository):
|
def __call__(
|
||||||
from . import EntitiesRepository
|
|
||||||
|
|
||||||
self._repo: EntitiesRepository = repository
|
|
||||||
|
|
||||||
def merge(
|
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
entities: Iterable[Entity],
|
entities: Iterable[Entity],
|
||||||
|
existing_entities: Optional[EntityMapping] = None,
|
||||||
) -> List[Entity]:
|
) -> List[Entity]:
|
||||||
"""
|
"""
|
||||||
Merge a set of entities with their existing representations and update
|
Merge a set of entities with their existing representations and update
|
||||||
the parent/child relationships and return a tuple with
|
the parent/child relationships and return a list containing
|
||||||
``[new_entities, updated_entities]``.
|
``[*updated_entities, *new_entities]``.
|
||||||
"""
|
"""
|
||||||
new_entities: Dict[Tuple[str, str], Entity] = {}
|
existing_entities = existing_entities or {}
|
||||||
existing_entities: Dict[Tuple[str, str], Entity] = {}
|
new_entities: EntityMapping = {}
|
||||||
|
|
||||||
self._merge(
|
self._merge(
|
||||||
session,
|
session,
|
||||||
|
@ -37,156 +33,164 @@ class EntitiesMerger:
|
||||||
existing_entities=existing_entities,
|
existing_entities=existing_entities,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [*existing_entities.values(), *new_entities.values()]
|
return list({**existing_entities, **new_entities}.values())
|
||||||
|
|
||||||
def _merge(
|
def _merge(
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
entities: Iterable[Entity],
|
entities: Iterable[Entity],
|
||||||
new_entities: Dict[Tuple[str, str], Entity],
|
new_entities: EntityMapping,
|
||||||
existing_entities: Dict[Tuple[str, str], Entity],
|
existing_entities: EntityMapping,
|
||||||
) -> List[Entity]:
|
) -> List[Entity]:
|
||||||
"""
|
"""
|
||||||
(Recursive) inner implementation of the entity merge logic.
|
(Recursive) inner implementation of the entity merge logic.
|
||||||
"""
|
"""
|
||||||
processed_entities = []
|
processed_entities = []
|
||||||
existing_entities.update(self._repo.get(session, entities))
|
|
||||||
|
|
||||||
# Make sure that we have no duplicate entity keys in the current batch
|
|
||||||
entities = list(
|
|
||||||
{
|
|
||||||
**({e.entity_key: e for e in entities}),
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
e.entity_key: e
|
|
||||||
for e in {str(ee.id): ee for ee in entities if ee.id}.values()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Retrieve existing records and merge them
|
# Retrieve existing records and merge them
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
key = entity.entity_key
|
key = entity.entity_key
|
||||||
existing_entity = existing_entities.get(key, new_entities.get(key))
|
existing_entity = existing_entities.get(key, new_entities.get(key))
|
||||||
parent_id, parent = self._update_parent(session, entity, new_entities)
|
|
||||||
|
# Synchronize the parent(s)
|
||||||
|
entity = self._sync_parent(session, entity, new_entities, existing_entities)
|
||||||
|
|
||||||
if existing_entity:
|
if existing_entity:
|
||||||
# Update the parent
|
# Merge the columns with those of the existing entity
|
||||||
if not parent_id and parent:
|
existing_entity = self._merge_columns(entity, existing_entity)
|
||||||
existing_entity.parent = parent
|
|
||||||
else:
|
|
||||||
existing_entity.parent_id = parent_id
|
|
||||||
|
|
||||||
# Merge the other columns
|
|
||||||
self._merge_columns(entity, existing_entity)
|
|
||||||
# Merge the children
|
# Merge the children
|
||||||
self._merge(session, entity.children, new_entities, existing_entities)
|
self._append_children(
|
||||||
# Use the updated version of the existing entity.
|
existing_entity,
|
||||||
|
*self._merge(
|
||||||
|
session,
|
||||||
|
entity.children,
|
||||||
|
new_entities,
|
||||||
|
existing_entities,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the existing entity now that it's been merged
|
||||||
entity = existing_entity
|
entity = existing_entity
|
||||||
else:
|
else:
|
||||||
# Add it to the map of new entities if the entity doesn't exist
|
# Add it to the map of new entities if the entity doesn't exist on the db
|
||||||
# on the repo
|
|
||||||
new_entities[key] = entity
|
new_entities[key] = entity
|
||||||
|
|
||||||
processed_entities.append(entity)
|
processed_entities.append(entity)
|
||||||
|
|
||||||
return processed_entities
|
return processed_entities
|
||||||
|
|
||||||
def _update_parent(
|
@classmethod
|
||||||
self,
|
def _sync_parent(
|
||||||
|
cls,
|
||||||
session: Session,
|
session: Session,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
new_entities: Dict[Tuple[str, str], Entity],
|
new_entities: EntityMapping,
|
||||||
) -> Tuple[Optional[int], Optional[Entity]]:
|
existing_entities: EntityMapping,
|
||||||
|
) -> Entity:
|
||||||
"""
|
"""
|
||||||
Recursively update the hierarchy of an entity, moving upwards towards
|
Recursively refresh the parent of an entity all the way up in the
|
||||||
the parent.
|
hierarchy, to make sure that all the parent/child relations are
|
||||||
|
appropriately rewired and that all the relevant objects are added to
|
||||||
|
this session.
|
||||||
"""
|
"""
|
||||||
parent_id: Optional[int] = entity.parent_id
|
parent = cls.get_parent(session, entity)
|
||||||
try:
|
if not parent:
|
||||||
parent: Optional[Entity] = entity.parent
|
# No parent -> we can terminate the recursive climbing
|
||||||
except exc.DetachedInstanceError:
|
return entity
|
||||||
# Dirty fix for `Parent instance <...> is not bound to a Session;
|
|
||||||
# lazy load operation of attribute 'parent' cannot proceed
|
|
||||||
parent = session.query(Entity).get(parent_id) if parent_id else None
|
|
||||||
|
|
||||||
# If the entity has a parent with an ID, use that
|
# Check if an entity with the same key as the reported parent already
|
||||||
if parent and parent.id:
|
# exists in the cached entities
|
||||||
parent_id = parent_id or parent.id
|
existing_parent = existing_entities.get(
|
||||||
|
parent.entity_key, new_entities.get(parent.entity_key)
|
||||||
|
)
|
||||||
|
|
||||||
# If there's no parent_id but there is a parent object, try to fetch
|
if not existing_parent:
|
||||||
# its stored version
|
# No existing parent -> we need to flush the one reported by this
|
||||||
if not parent_id and parent:
|
# entity
|
||||||
batch = list(self._repo.get(session, [parent]).values())
|
return entity
|
||||||
|
|
||||||
# If the parent is already stored, use its ID
|
# Check if the existing parent already has a child with the same key as
|
||||||
if batch:
|
# this entity
|
||||||
parent = batch[0]
|
existing_entity = next(
|
||||||
parent_id = parent.id
|
iter(
|
||||||
|
child
|
||||||
|
for child in existing_parent.children
|
||||||
|
if child.entity_key == entity.entity_key
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
# Otherwise, check if its key is already among those awaiting flush
|
if not existing_entity:
|
||||||
# and reuse the same objects (prevents SQLAlchemy from generating
|
# If this entity isn't currently a member of the existing parent,
|
||||||
# duplicate inserts)
|
# temporarily reset the parent of the current entity, so we won't
|
||||||
else:
|
# carry stale objects around. We will soon rewire it to the
|
||||||
temp_entity = new_entities.get(parent.entity_key)
|
# existing parent.
|
||||||
if temp_entity:
|
|
||||||
self._remove_duplicate_children(entity, temp_entity)
|
|
||||||
parent = entity.parent = temp_entity
|
|
||||||
else:
|
|
||||||
new_entities[parent.entity_key] = parent
|
|
||||||
|
|
||||||
# Recursively apply any changes up in the hierarchy
|
|
||||||
self._update_parent(session, parent, new_entities=new_entities)
|
|
||||||
|
|
||||||
# If we found a parent_id, populate it on the entity (and remove the
|
|
||||||
# supporting relationship object so SQLAlchemy doesn't go nuts when
|
|
||||||
# flushing)
|
|
||||||
if parent_id:
|
|
||||||
entity.parent = None
|
entity.parent = None
|
||||||
entity.parent_id = parent_id
|
else:
|
||||||
|
# Otherwise, merge the columns of the existing entity with those of
|
||||||
|
# the new entity and use the existing entity
|
||||||
|
entity = cls._merge_columns(entity, existing_entity)
|
||||||
|
|
||||||
return parent_id, parent
|
# Refresh the existing collection of children with the new/updated
|
||||||
|
# entity
|
||||||
|
cls._append_children(existing_parent, entity)
|
||||||
|
|
||||||
|
# Recursively call this function to synchronize any parent entities up
|
||||||
|
# in the taxonomy
|
||||||
|
cls._sync_parent(session, existing_parent, new_entities, existing_entities)
|
||||||
|
return entity
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _remove_duplicate_children(entity: Entity, parent: Optional[Entity] = None):
|
def get_parent(session: Session, entity: Entity) -> Optional[Entity]:
|
||||||
if not parent:
|
"""
|
||||||
return
|
Gets the parent of an entity, and it fetches if it's not available in
|
||||||
|
the current session.
|
||||||
# Make sure that an entity has no duplicate entity IDs among its
|
"""
|
||||||
# children
|
|
||||||
existing_child_index_by_id = None
|
|
||||||
if entity.id:
|
|
||||||
try:
|
|
||||||
existing_child_index_by_id = [e.id for e in parent.children].index(
|
|
||||||
entity.id
|
|
||||||
)
|
|
||||||
parent.children.pop(existing_child_index_by_id)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Make sure that an entity has no duplicate entity keys among its
|
|
||||||
# children
|
|
||||||
existing_child_index_by_key = None
|
|
||||||
try:
|
try:
|
||||||
existing_child_index_by_key = [e.entity_key for e in parent.children].index(
|
return entity.parent
|
||||||
entity.entity_key
|
except exc.DetachedInstanceError:
|
||||||
|
# Dirty fix for `Parent instance <...> is not bound to a Session;
|
||||||
|
# lazy load operation of attribute 'parent' cannot proceed`
|
||||||
|
return (
|
||||||
|
session.query(Entity).get(entity.parent_id)
|
||||||
|
if entity.parent_id
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
parent.children.pop(existing_child_index_by_key)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def _merge_columns(cls, entity: Entity, existing_entity: Entity) -> Entity:
|
def _append_children(entity: Entity, *children: Entity):
|
||||||
|
"""
|
||||||
|
Update the list of children of a given entity with the given list of
|
||||||
|
entities.
|
||||||
|
|
||||||
|
Note that, in case of ``entity_key`` conflict (the key of a new entity
|
||||||
|
already exists in the entity's children), the most recent version will
|
||||||
|
be used, so any column merge logic needs to happen before this method
|
||||||
|
is called.
|
||||||
|
"""
|
||||||
|
entity.children = list(
|
||||||
|
{
|
||||||
|
**{e.entity_key: e for e in entity.children},
|
||||||
|
**{e.entity_key: e for e in children},
|
||||||
|
}.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
child.parent = entity
|
||||||
|
if entity.id:
|
||||||
|
child.parent_id = entity.id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _merge_columns(entity: Entity, existing_entity: Entity) -> Entity:
|
||||||
"""
|
"""
|
||||||
Merge two versions of an entity column by column.
|
Merge two versions of an entity column by column.
|
||||||
"""
|
"""
|
||||||
columns = [col.key for col in entity.columns]
|
columns = [col.key for col in entity.columns]
|
||||||
for col in columns:
|
for col in columns:
|
||||||
if col == 'meta':
|
if col == 'meta':
|
||||||
existing_entity.meta = {
|
existing_entity.meta = { # type: ignore
|
||||||
**(existing_entity.meta or {}),
|
**(existing_entity.meta or {}), # type: ignore
|
||||||
**(entity.meta or {}),
|
**(entity.meta or {}), # type: ignore
|
||||||
}
|
}
|
||||||
elif col not in ('id', 'created_at'):
|
elif col not in ('id', 'created_at'):
|
||||||
setattr(existing_entity, col, getattr(entity, col))
|
setattr(existing_entity, col, getattr(entity, col))
|
||||||
|
|
|
@ -158,10 +158,6 @@ if 'bluetooth_device' not in Base.metadata:
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Overwrites ``to_dict`` to transform private column names into their
|
Overwrites ``to_dict`` to transform private column names into their
|
||||||
public representation, and also include the exposed services and
|
public representation.
|
||||||
child entities.
|
|
||||||
"""
|
"""
|
||||||
return {
|
return {k.lstrip('_'): v for k, v in super().to_dict().items()}
|
||||||
**{k.lstrip('_'): v for k, v in super().to_dict().items()},
|
|
||||||
'children': [child.to_dict() for child in self.children],
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,6 +48,9 @@ if 'bluetooth_service' not in Base.metadata:
|
||||||
is_ble = Column(Boolean, default=False)
|
is_ble = Column(Boolean, default=False)
|
||||||
""" Whether the service is a BLE service. """
|
""" Whether the service is a BLE service. """
|
||||||
|
|
||||||
|
connected = Column(Boolean, default=False)
|
||||||
|
""" Whether an active connection exists to this service. """
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {
|
||||||
'polymorphic_identity': __tablename__,
|
'polymorphic_identity': __tablename__,
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,7 +216,7 @@ def _parse_services(device: BLEDevice) -> List[BluetoothService]:
|
||||||
BluetoothService(
|
BluetoothService(
|
||||||
id=f'{device.address}:{uuid}',
|
id=f'{device.address}:{uuid}',
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
name=str(srv_cls),
|
name=f'[{uuid}]' if srv_cls == ServiceClass.UNKNOWN else str(srv_cls),
|
||||||
protocol=Protocol.L2CAP,
|
protocol=Protocol.L2CAP,
|
||||||
is_ble=True,
|
is_ble=True,
|
||||||
)
|
)
|
||||||
|
@ -236,9 +236,7 @@ def device_to_entity(device: BLEDevice, data: AdvertisementData) -> BluetoothDev
|
||||||
theengs_entity = _parse_advertisement_data(data)
|
theengs_entity = _parse_advertisement_data(data)
|
||||||
props = (device.details or {}).get('props', {})
|
props = (device.details or {}).get('props', {})
|
||||||
manufacturer = theengs_entity.manufacturer or company.get(
|
manufacturer = theengs_entity.manufacturer or company.get(
|
||||||
list(device.metadata['manufacturer_data'].keys())[0]
|
next(iter(key for key in device.metadata['manufacturer_data']), 0xFFFF)
|
||||||
if device.metadata.get('manufacturer_data', {})
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
parent_entity = BluetoothDevice(
|
parent_entity = BluetoothDevice(
|
||||||
|
@ -279,8 +277,8 @@ def device_to_entity(device: BLEDevice, data: AdvertisementData) -> BluetoothDev
|
||||||
# Skip entities that we couldn't parse.
|
# Skip entities that we couldn't parse.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entity.id = f'{parent_entity.id}:{prop}'
|
entity.id = f'{parent_entity.address}::{prop}'
|
||||||
entity.name = prop
|
entity.name = prop.title()
|
||||||
parent_entity.children.append(entity)
|
parent_entity.children.append(entity)
|
||||||
entity.parent = parent_entity
|
entity.parent = parent_entity
|
||||||
|
|
||||||
|
|
|
@ -174,15 +174,23 @@ class LegacyManager(BaseBluetoothManager):
|
||||||
raise AssertionError(f'Connection to {device} timed out') from e
|
raise AssertionError(f'Connection to {device} timed out') from e
|
||||||
|
|
||||||
dev.connected = True
|
dev.connected = True
|
||||||
|
conn.service.connected = True
|
||||||
self.notify(BluetoothDeviceConnectedEvent, dev)
|
self.notify(BluetoothDeviceConnectedEvent, dev)
|
||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
# Close the connection once the context is over
|
# Close the connection once the context is over
|
||||||
with self._connection_locks[conn.key]:
|
with self._connection_locks[conn.key]:
|
||||||
conn.close()
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'Error while closing the connection to %s: %s', device, e
|
||||||
|
)
|
||||||
|
|
||||||
self._connections.pop(conn.key, None)
|
self._connections.pop(conn.key, None)
|
||||||
|
|
||||||
dev.connected = False
|
dev.connected = False
|
||||||
|
conn.service.connected = False
|
||||||
self.notify(BluetoothDeviceDisconnectedEvent, dev)
|
self.notify(BluetoothDeviceDisconnectedEvent, dev)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -86,7 +86,7 @@ class SwitchTplinkPlugin(RunnablePlugin, SwitchEntityManager):
|
||||||
devices: Optional[Mapping[str, SmartDevice]] = None,
|
devices: Optional[Mapping[str, SmartDevice]] = None,
|
||||||
publish_entities: bool = True,
|
publish_entities: bool = True,
|
||||||
):
|
):
|
||||||
for (addr, info) in self._static_devices.items():
|
for addr, info in self._static_devices.items():
|
||||||
try:
|
try:
|
||||||
dev = info['type'](addr)
|
dev = info['type'](addr)
|
||||||
self._alias_to_dev[info.get('name', dev.alias)] = dev
|
self._alias_to_dev[info.get('name', dev.alias)] = dev
|
||||||
|
@ -94,7 +94,7 @@ class SwitchTplinkPlugin(RunnablePlugin, SwitchEntityManager):
|
||||||
except SmartDeviceException as e:
|
except SmartDeviceException as e:
|
||||||
self.logger.warning('Could not communicate with device %s: %s', addr, e)
|
self.logger.warning('Could not communicate with device %s: %s', addr, e)
|
||||||
|
|
||||||
for (ip, dev) in (devices or {}).items():
|
for ip, dev in (devices or {}).items():
|
||||||
self._ip_to_dev[ip] = dev
|
self._ip_to_dev[ip] = dev
|
||||||
self._alias_to_dev[dev.alias] = dev
|
self._alias_to_dev[dev.alias] = dev
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ class SwitchTplinkPlugin(RunnablePlugin, SwitchEntityManager):
|
||||||
return [self._serialize(dev) for dev in self._scan().values()]
|
return [self._serialize(dev) for dev in self._scan().values()]
|
||||||
|
|
||||||
def main(self):
|
def main(self):
|
||||||
devices = {ip: self._serialize(dev) for ip, dev in self._ip_to_dev}
|
devices = {ip: self._serialize(dev) for ip, dev in self._ip_to_dev.items()}
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
new_devices = self._scan(publish_entities=False)
|
new_devices = self._scan(publish_entities=False)
|
||||||
|
|
Loading…
Reference in a new issue