Compare commits

...

10 Commits

27 changed files with 265 additions and 64 deletions

View File

@ -299,7 +299,7 @@ autodoc_mock_imports = [
'async_lru',
'bleak',
'bluetooth_numbers',
'TheengsGateway',
'TheengsDecoder',
]
sys.path.insert(0, os.path.abspath('../..'))

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.95bedba1.js"></script><script defer="defer" type="module" src="/static/js/app.027226cc.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.aa0132dc.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.79dede0c.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.a7f221b9.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.d0179f42.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.aa0132dc.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.79dede0c.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.d0b74440.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -121,13 +121,19 @@ $label-width: 3em;
.range-labels {
width: 100%;
display: flex;
&.with-label {
width: calc(100% - $label-width);
}
.left {
text-align: left;
}
.right {
@extend .pull-right;
flex-grow: 1;
}
}

View File

@ -0,0 +1,73 @@
<template>
<div class="entity device-container">
<div class="head">
<div class="col-1 icon">
<EntityIcon
:entity="value"
:loading="loading"
:error="error" />
</div>
<div class="col-2 connector">
<ToggleSwitch
:value="value.connected"
:disabled="loading"
@input="connect"
@click.stop />
</div>
<div class="col-9 label">
<div class="name" v-text="value.name" />
</div>
</div>
</div>
</template>
<script>
import EntityMixin from "./EntityMixin"
import EntityIcon from "./EntityIcon"
import ToggleSwitch from "@/components/elements/ToggleSwitch"
export default {
name: 'BluetoothDevice',
components: {EntityIcon, ToggleSwitch},
mixins: [EntityMixin],
methods: {
async connect(event) {
event.stopPropagation()
this.$emit('loading', true)
const method = (
'bluetooth.' +
(this.value.connected ? 'disconnect' : 'connect')
)
try {
await this.request(method, {
device: this.value.address,
})
} finally {
this.$emit('loading', false)
}
},
},
}
</script>
<style lang="scss" scoped>
@import "common";
.device-container {
display: flex;
justify-content: center;
.icon {
margin-right: 1em;
}
.connector {
width: 4em;
margin: 0.25em 0 -0.25em 0.5em;
}
}
</style>

View File

@ -14,7 +14,7 @@
<div class="col-2 connector pull-right">
<ToggleSwitch
:value="parent.connected"
:value="value.connected"
:disabled="loading"
@input="connect"
@click.stop />
@ -37,9 +37,13 @@ export default {
async connect(event) {
event.stopPropagation()
this.$emit('loading', true)
const method = (
'bluetooth.' +
(this.value.connected ? 'disconnect' : 'connect')
)
try {
await this.request('bluetooth.connect', {
await this.request(method, {
device: this.parent.address,
service_uuid: this.uuid,
})

View File

@ -8,7 +8,7 @@
:error="error" />
</div>
<div class="col-12 label">
<div class="col-11 label">
<div class="name" v-text="value.name" />
</div>
</div>

View File

@ -13,13 +13,13 @@
</div>
<div class="col-s-4 col-m-3 buttons pull-right">
<span class="value-percent"
v-text="parsedValue"
v-if="parsedValue != null" />
<button @click.stop="collapsed = !collapsed">
<i class="fas"
:class="{'fa-angle-up': !collapsed, 'fa-angle-down': collapsed}" />
</button>
<span class="value-percent"
v-text="parsedValue"
v-if="parsedValue != null" />
</div>
</div>

View File

@ -8,6 +8,7 @@
:is="component"
:value="value"
:parent="parent"
:children="computedChildren"
:loading="loading"
ref="instance"
:error="error || value?.reachable == false"
@ -30,6 +31,7 @@
:parent="value"
:loading="loading"
:level="level + 1"
@show-modal="$emit('show-modal', $event)"
@input="$emit('input', entity)" />
</div>
</div>
@ -44,7 +46,7 @@ import { bus } from "@/bus";
export default {
name: "Entity",
mixins: [EntityMixin],
emits: ['input', 'loading', 'update'],
emits: ['input', 'loading', 'update', 'show-modal'],
data() {
return {
@ -87,12 +89,19 @@ export default {
},
onClick(event) {
event.stopPropagation()
if (
event.target.classList.contains('label') ||
event.target.classList.contains('head')
) {
event.stopPropagation()
// When clicking on the name or icon of the entity, stop the event
// propagation and toggle the collapsed state instead.
this.toggleCollapsed()
} else {
// Otherwise, propagate the event upwards as a request to show the
// entity details modal.
this.$emit('show-modal', this.value.id)
}
},
@ -210,6 +219,10 @@ export default {
}
}
.label {
margin-left: 0.5em;
}
.icon:hover {
color: $hover-fg;
}

View File

@ -13,13 +13,13 @@
</div>
<div class="col-s-3 col-m-2 buttons pull-right">
<span class="value"
v-text="value.values[value.value] || value.value"
v-if="value?.value != null" />
<button @click.stop="collapsed = !collapsed" v-if="hasValues">
<i class="fas"
:class="{'fa-angle-up': !collapsed, 'fa-angle-down': collapsed}" />
</button>
<span class="value"
v-text="value.values[value.value] || value.value"
v-if="value?.value != null" />
</div>
</div>

View File

@ -20,6 +20,7 @@
:visible="modalVisible"
:config-values="configValuesByParentId(modalEntityId)"
@close="onEntityModal"
@entity-update="modalEntityId = $event"
v-if="modalEntityId && entities[modalEntityId]"
/>
@ -48,11 +49,13 @@
</div>
<div class="body">
<div class="entity-frame" @click="onEntityModal(entity.id)"
v-for="entity in group.entities" :key="entity.id">
<div class="entity-frame"
v-for="entity in group.entities"
:key="entity.id">
<Entity
:value="entity"
:children="childrenByParentId(entity.id)"
@show-modal="onEntityModal($event)"
@input="onEntityInput(entity)"
:error="!!errorEntities[entity.id]"
:loading="!!loadingEntities[entity.id]"

View File

@ -90,10 +90,13 @@
</div>
</div>
<div v-for="value, attr in entity.data || {}" :key="attr">
<div class="table-row" v-if="value != null">
<div class="title" v-text="prettify(attr)" />
<div class="value" v-text="'' + value" />
<div class="table-row" v-if="entity.parent_id">
<div class="title">Parent</div>
<div class="value">
<a href="#"
@click="$emit('entity-update', entity.parent_id)"
v-text="entity.parent_id"
/>
</div>
</div>
@ -107,18 +110,49 @@
<div class="value" v-text="formatDateTime(entity.updated_at)" />
</div>
<div class="table-row delete-entity-container">
<div class="table-row delete-entity-container"
@click="$refs.deleteConfirmDiag.show()">
<div class="title">Delete Entity</div>
<div class="value">
<button @click="$refs.deleteConfirmDiag.show()">
<button @click.stop="$refs.deleteConfirmDiag.show()">
<i class="fas fa-trash" />
</button>
</div>
</div>
<div class="extra-info-container">
<div class="title section-title" @click="extraInfoCollapsed = !extraInfoCollapsed">
<div class="col-11">
<i class="fas fa-circle-info" /> &nbsp;
Extra Info
</div>
<div class="col-1 pull-right">
<i class="fas"
:class="{'fa-chevron-down': extraInfoCollapsed, 'fa-chevron-up': !extraInfoCollapsed}" />
</div>
</div>
<div class="extra-info" v-if="!extraInfoCollapsed">
<div v-for="value, attr in entity" :key="attr">
<div class="table-row" v-if="value != null && specialFields.indexOf(attr) < 0">
<div class="title" v-text="prettify(attr)" />
<div class="value" v-text="stringify(value)" />
</div>
</div>
<div v-for="value, attr in (entity.data || {})" :key="attr">
<div class="table-row" v-if="value != null">
<div class="title" v-text="prettify(attr)" />
<div class="value" v-text="stringify(value)" />
</div>
</div>
</div>
</div>
<div class="config-container"
v-if="computedConfig.length">
<div class="title"
<div class="title section-title"
@click="configCollapsed = !configCollapsed">
<div class="col-11">
<i class="fas fa-screwdriver-wrench" /> &nbsp;
@ -152,11 +186,27 @@ import Utils from "@/Utils";
import Entity from "./Entity";
import meta from './meta.json';
// These fields have a different rendering logic than the general-purpose one
const specialFields = [
'created_at',
'data',
'description',
'external_id',
'external_url',
'id',
'image_url',
'meta',
'name',
'plugin',
'updated_at',
'parent_id',
]
export default {
name: "EntityModal",
components: {Entity, Modal, EditButton, NameEditor, Icon, ConfirmDialog},
mixins: [Utils],
emits: ['input', 'loading'],
emits: ['input', 'loading', 'entity-update'],
props: {
entity: {
type: Object,
@ -188,6 +238,8 @@ export default {
editName: false,
editIcon: false,
configCollapsed: true,
extraInfoCollapsed: true,
specialFields: specialFields,
}
},
@ -257,6 +309,14 @@ export default {
this.editIcon = false
}
},
stringify(value) {
if (value == null)
return ''
if (Array.isArray(value) || typeof value === 'object')
return JSON.stringify(value)
return '' + value
},
},
}
</script>
@ -306,26 +366,39 @@ export default {
.delete-entity-container {
color: $error-fg;
cursor: pointer;
button {
color: $error-fg;
}
}
.config-container {
@mixin section-title {
display: flex;
cursor: pointer;
padding: 1em;
text-transform: uppercase;
letter-spacing: 0.033em;
border-top: $default-border;
box-shadow: $border-shadow-bottom;
&:hover {
background: $hover-bg;
}
}
.config-container,
.extra-info-container
{
margin: 0;
.title {
display: flex;
padding: 1em;
text-transform: uppercase;
letter-spacing: 0.033em;
border-top: $default-border;
box-shadow: $border-shadow-bottom;
cursor: pointer;
.section-title {
@include section-title;
}
}
&:hover {
background: $hover-bg;
}
.config-container {
.title {
@include section-title;
}
}

View File

@ -14,9 +14,9 @@
<div class="col-s-3 col-m-2 pull-right"
v-if="value.value != null">
<span class="value" v-text="value.value" />
<span class="unit" v-text="value.unit"
v-if="value.unit != null" />
<span class="value" v-text="value.value" />
</div>
</div>
</div>

View File

@ -21,12 +21,10 @@
}
.pull-right {
display: inline-flex;
align-items: center;
direction: rtl;
padding-right: 0.5em;
:deep(.power-switch) {
@include pull-right;
margin-top: 0.25em;
}
}

View File

@ -150,21 +150,26 @@ $widths: (
}
.vertical-center {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.horizontal-center {
display: flex;
justify-content: center;
margin-left: auto;
margin-right: auto;
display: flex;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
@mixin pull-right {
display: inline-flex;
text-align: right;
justify-content: right;
flex-grow: 1;
}
.pull-right {
text-align: right;
float: right;
justify-content: right;
@include pull-right;
}
.hidden {

View File

@ -42,6 +42,10 @@ export default {
if (typeof(a) !== 'object' || typeof(b) !== 'object')
return false
if (a == null || b == null) {
return a == null && b == null
}
for (const p of Object.keys(a || {})) {
switch(typeof(a[p])) {
case 'object':

View File

@ -193,7 +193,7 @@ if 'entity' not in Base.metadata:
# standard multiple inheritance with an SQLAlchemy ORM class)
Entity.__bases__ = Entity.__bases__ + (JSONAble,)
EntitySavedCallback = Callable[[Entity], None]
EntitySavedCallback = Callable[[Entity], Any]
"""
Type for the callback functions that should be called when an entity is saved
on the database.

View File

@ -7,8 +7,7 @@ from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from bluetooth_numbers import company
# pylint: disable=no-name-in-module
from TheengsGateway._decoder import decodeBLE, getAttribute, getProperties
from TheengsDecoder import decodeBLE, getAttribute, getProperties
from platypush.entities import Entity
from platypush.entities.batteries import Battery

View File

@ -9,6 +9,7 @@ from typing import (
Final,
List,
Optional,
Set,
Union,
Type,
)
@ -50,7 +51,7 @@ class BluetoothPlugin(RunnablePlugin, EntityManager):
* **bleak** (``pip install bleak``)
* **bluetooth-numbers** (``pip install bluetooth-numbers``)
* **TheengsGateway** (``pip install git+https://github.com/theengs/gateway``)
* **TheengsDecoder** (``pip install TheengsDecoder``)
* **pybluez** (``pip install git+https://github.com/pybluez/pybluez``)
* **pyobex** (``pip install git+https://github.com/BlackLight/PyOBEX``)
@ -76,6 +77,16 @@ class BluetoothPlugin(RunnablePlugin, EntityManager):
_default_scan_duration: Final[float] = 10.0
""" Default duration of a discovery session (in seconds) """
_default_excluded_manufacturers = {
'Apple, Inc.',
'Google',
'Microsoft',
}
"""
Exclude beacons from these device manufacturers by default (main offenders
when it comes to Bluetooth device space pollution).
"""
def __init__(
self,
interface: Optional[str] = None,
@ -83,6 +94,7 @@ class BluetoothPlugin(RunnablePlugin, EntityManager):
service_uuids: Optional[Collection[RawServiceClass]] = None,
scan_paused_on_start: bool = False,
poll_interval: float = _default_scan_duration,
excluded_manufacturers: Optional[Collection[str]] = None,
**kwargs,
):
"""
@ -95,7 +107,14 @@ class BluetoothPlugin(RunnablePlugin, EntityManager):
:param scan_paused_on_start: If ``True``, the plugin will not the
scanning thread until :meth:`.scan_resume` is called (default:
``False``).
:param excluded_manufacturers: Exclude beacons from these device
manufacturers. The default list includes Apple, Google and
Microsoft, who are among the main offenders when it comes to
Bluetooth device address space pollution. Set this value to an
empty list if you want to get all beacons (e.g. because you need to
communicate with Apple AirTags or Google devices over Bluetooth),
but be warned that the list of discovered Bluetooth devices may
dramatically increase over time.
"""
kwargs['poll_interval'] = poll_interval
super().__init__(**kwargs)
@ -119,6 +138,10 @@ class BluetoothPlugin(RunnablePlugin, EntityManager):
"""
Cache of the devices discovered by the plugin.
"""
self._excluded_manufacturers: Set[str] = set(excluded_manufacturers or [])
"""
Set of manufacturer strings whose associated devices should be ignored.
"""
self._managers: Dict[Type[BaseBluetoothManager], BaseBluetoothManager] = {}
"""
@ -553,7 +576,8 @@ class BluetoothPlugin(RunnablePlugin, EntityManager):
continue
device = self._device_cache.add(device)
self.publish_entities([device], callback=self._device_cache.add)
if device.manufacturer not in self._excluded_manufacturers:
self.publish_entities([device], callback=self._device_cache.add)
finally:
self.stop()

View File

@ -16,8 +16,8 @@ manifest:
pip:
- bleak
- bluetooth-numbers
- TheengsDecoder
- git+https://github.com/pybluez/pybluez
- git+https://github.com/theengs/gateway
- git+https://github.com/BlackLight/PyOBEX
package: platypush.plugins.bluetooth
type: plugin

View File

@ -17,7 +17,7 @@ def readfile(fname):
def pkg_files(dir):
paths = []
# noinspection PyShadowingNames
for (path, _, files) in os.walk(dir):
for path, _, files in os.walk(dir):
for file in files:
paths.append(os.path.join('..', path, file))
return paths
@ -177,9 +177,9 @@ setup(
'bluetooth': [
'bleak',
'bluetooth-numbers',
'TheengsDecoder',
'pybluez @ https://github.com/pybluez/pybluez/tarball/master',
'PyOBEX @ https://github.com/BlackLight/PyOBEX/tarball/master',
'TheengsGateway @ https://github.com/theengs/gateway/tarball/development',
],
# Support for TP-Link devices
'tplink': ['pyHS100'],