Compare commits
4 Commits
575635fd6b
...
45664be44b
Author | SHA1 | Date |
---|---|---|
Fabio Manganiello | 45664be44b | |
Fabio Manganiello | 471bc1fd3d | |
Fabio Manganiello | a3aa186ddf | |
Fabio Manganiello | 1d0be5c929 |
|
@ -1,109 +0,0 @@
|
|||
import time
|
||||
from threading import Thread, RLock
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
from platypush.backend.sensor import SensorBackend
|
||||
from platypush.context import get_plugin
|
||||
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
|
||||
|
||||
|
||||
class BluetoothScannerBackend(SensorBackend):
|
||||
"""
|
||||
This backend periodically scans for available bluetooth devices and returns events when a devices enter or exits
|
||||
the range.
|
||||
|
||||
Triggers:
|
||||
|
||||
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent` when a new bluetooth device is found.
|
||||
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent` when a bluetooth device is lost.
|
||||
|
||||
Requires:
|
||||
|
||||
* The :class:`platypush.plugins.bluetooth.BluetoothPlugin` plugin working.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10,
|
||||
track_devices: Optional[List[str]] = None, **kwargs):
|
||||
"""
|
||||
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
|
||||
:param scan_duration: How long the scan should run (default: 10 seconds).
|
||||
:param track_devices: List of addresses of devices to actively track, even if they aren't discoverable.
|
||||
"""
|
||||
super().__init__(plugin='bluetooth', plugin_args={
|
||||
'device_id': device_id,
|
||||
'duration': scan_duration,
|
||||
}, **kwargs)
|
||||
|
||||
self._last_seen_devices = {}
|
||||
self._tracking_thread: Optional[Thread] = None
|
||||
self._bt_lock = RLock()
|
||||
self.track_devices = set(track_devices or [])
|
||||
self.scan_duration = scan_duration
|
||||
|
||||
def _add_last_seen_device(self, dev):
|
||||
addr = dev.pop('addr')
|
||||
if addr not in self._last_seen_devices:
|
||||
self.bus.post(BluetoothDeviceFoundEvent(address=addr, **dev))
|
||||
self._last_seen_devices[addr] = {'addr': addr, **dev}
|
||||
|
||||
def _remove_last_seen_device(self, addr: str):
|
||||
dev = self._last_seen_devices.get(addr)
|
||||
if not dev:
|
||||
return
|
||||
|
||||
self.bus.post(BluetoothDeviceLostEvent(address=addr, **dev))
|
||||
del self._last_seen_devices[addr]
|
||||
|
||||
def _addr_tracker(self, addr):
|
||||
with self._bt_lock:
|
||||
name = get_plugin('bluetooth').lookup_name(addr, timeout=self.scan_duration).name
|
||||
|
||||
if name is None:
|
||||
self._remove_last_seen_device(addr)
|
||||
else:
|
||||
self._add_last_seen_device({'addr': addr, 'name': name})
|
||||
|
||||
def _bt_tracker(self):
|
||||
self.logger.info('Starting Bluetooth tracker')
|
||||
while not self.should_stop():
|
||||
trackers = []
|
||||
for addr in self.track_devices:
|
||||
tracker = Thread(target=self._addr_tracker, args=(addr,))
|
||||
tracker.start()
|
||||
trackers.append(tracker)
|
||||
|
||||
for tracker in trackers:
|
||||
tracker.join(timeout=self.scan_duration)
|
||||
|
||||
time.sleep(self.scan_duration)
|
||||
|
||||
self.logger.info('Bluetooth tracker stopped')
|
||||
|
||||
def get_measurement(self):
|
||||
with self._bt_lock:
|
||||
return super().get_measurement()
|
||||
|
||||
def process_data( # lgtm [py/inheritance/signature-mismatch]
|
||||
self, data: Dict[str, dict], new_data: Optional[Dict[str, dict]] = None, **_
|
||||
):
|
||||
for addr, dev in data.items():
|
||||
self._add_last_seen_device(dev)
|
||||
|
||||
for addr, dev in self._last_seen_devices.copy().items():
|
||||
if addr not in data and addr not in self.track_devices:
|
||||
self._remove_last_seen_device(addr)
|
||||
|
||||
def run(self):
|
||||
self._tracking_thread = Thread(target=self._bt_tracker)
|
||||
self._tracking_thread.start()
|
||||
super().run()
|
||||
|
||||
def on_stop(self):
|
||||
super().on_stop()
|
||||
if self._tracking_thread and self._tracking_thread.is_alive():
|
||||
self.logger.info('Waiting for the Bluetooth tracking thread to stop')
|
||||
self._tracking_thread.join(timeout=self.scan_duration)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -1,10 +0,0 @@
|
|||
manifest:
|
||||
events:
|
||||
platypush.message.event.bluetooth.BluetoothDeviceFoundEvent: when a new bluetooth
|
||||
device is found.
|
||||
platypush.message.event.bluetooth.BluetoothDeviceLostEvent: when a bluetooth device
|
||||
is lost.
|
||||
install:
|
||||
pip: []
|
||||
package: platypush.backend.bluetooth.scanner
|
||||
type: backend
|
|
@ -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.9397ac28.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.22d80610.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.aa9a5927.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.6a27238d.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
Device.vue
|
|
@ -31,6 +31,14 @@
|
|||
}
|
||||
},
|
||||
|
||||
"bluetooth_device": {
|
||||
"name": "Device",
|
||||
"name_plural": "Devices",
|
||||
"icon": {
|
||||
"class": "fab fa-bluetooth-b"
|
||||
}
|
||||
},
|
||||
|
||||
"device": {
|
||||
"name": "Device",
|
||||
"name_plural": "Devices",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from typing_extensions import override
|
||||
|
||||
from . import EntityManager
|
||||
|
@ -11,15 +11,13 @@ class WriteableEntityManager(EntityManager, ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
|
||||
def set(self, entity: str, value: Any, **kwargs):
|
||||
"""
|
||||
Set the value of an entity.
|
||||
|
||||
:param entity: The entity to set the value for. It's usually the ID of
|
||||
the entity provided by the plugin.
|
||||
:param value: The value to set the entity to.
|
||||
:param attribute: The name of the attribute to set for the entity, if
|
||||
required by the integration.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -45,7 +43,7 @@ class SwitchEntityManager(WriteableEntityManager, ABC):
|
|||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
|
||||
def set(self, entity: str, value: Any, **kwargs):
|
||||
method = self.on if value else self.off
|
||||
return method(entity, **kwargs)
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
from sqlalchemy import Column, Integer, Boolean, ForeignKey
|
||||
|
||||
from platypush.common.db import Base
|
||||
|
||||
from .devices import Device
|
||||
|
||||
|
||||
if 'bluetooth_device' not in Base.metadata:
|
||||
|
||||
class BluetoothDevice(Device):
|
||||
"""
|
||||
Entity that represents a Bluetooth device.
|
||||
"""
|
||||
|
||||
__tablename__ = 'bluetooth_device'
|
||||
|
||||
id = Column(
|
||||
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||
)
|
||||
connected = Column(Boolean, default=False)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
}
|
|
@ -5,7 +5,31 @@ from platypush.message.event import Event
|
|||
|
||||
class BluetoothEvent(Event):
|
||||
"""
|
||||
Base class for Bluetooth Low-Energy device events.
|
||||
Base class for Bluetooth events.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothScanPausedEvent(BluetoothEvent):
|
||||
"""
|
||||
Event triggered when the Bluetooth scan is paused.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, duration: Optional[float] = None, **kwargs):
|
||||
super().__init__(*args, duration=duration, **kwargs)
|
||||
|
||||
|
||||
class BluetoothScanResumedEvent(BluetoothEvent):
|
||||
"""
|
||||
Event triggered when the Bluetooth scan is resumed.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, duration: Optional[float] = None, **kwargs):
|
||||
super().__init__(*args, duration=duration, **kwargs)
|
||||
|
||||
|
||||
class BluetoothDeviceEvent(BluetoothEvent):
|
||||
"""
|
||||
Base class for Bluetooth device events.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -17,7 +41,7 @@ class BluetoothEvent(Event):
|
|||
trusted: bool,
|
||||
blocked: bool,
|
||||
name: Optional[str] = None,
|
||||
service_uuids: Optional[Collection[str]] = None,
|
||||
characteristics: Optional[Collection[str]] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
|
@ -27,7 +51,8 @@ class BluetoothEvent(Event):
|
|||
:param trusted: Whether the device is trusted.
|
||||
:param blocked: Whether the device is blocked.
|
||||
:param name: The name of the device.
|
||||
:param service_uuids: The service UUIDs of the device.
|
||||
:param characteristics: The UUIDs of the characteristics exposed by the
|
||||
device.
|
||||
"""
|
||||
super().__init__(
|
||||
*args,
|
||||
|
@ -37,66 +62,66 @@ class BluetoothEvent(Event):
|
|||
paired=paired,
|
||||
blocked=blocked,
|
||||
trusted=trusted,
|
||||
service_uuids=service_uuids or [],
|
||||
characteristics=characteristics or [],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class BluetoothDeviceFoundEvent(BluetoothEvent):
|
||||
class BluetoothDeviceFoundEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is discovered during a scan.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceLostEvent(BluetoothEvent):
|
||||
class BluetoothDeviceLostEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a previously discovered Bluetooth device is lost.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceConnectedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceConnectedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is connected.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceDisconnectedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceDisconnectedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is disconnected.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDevicePairedEvent(BluetoothEvent):
|
||||
class BluetoothDevicePairedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is paired.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceUnpairedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceUnpairedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is unpaired.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceBlockedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceBlockedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is blocked.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceUnblockedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceUnblockedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is unblocked.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceTrustedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceTrustedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is trusted.
|
||||
"""
|
||||
|
||||
|
||||
class BluetoothDeviceUntrustedEvent(BluetoothEvent):
|
||||
class BluetoothDeviceUntrustedEvent(BluetoothDeviceEvent):
|
||||
"""
|
||||
Event triggered when a Bluetooth device is untrusted.
|
||||
"""
|
||||
|
|
|
@ -5,6 +5,7 @@ import threading
|
|||
from abc import ABC, abstractmethod
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional
|
||||
from typing_extensions import override
|
||||
|
||||
from platypush.bus import Bus
|
||||
from platypush.common import ExtensionWithManifest
|
||||
|
@ -97,21 +98,33 @@ class RunnablePlugin(Plugin):
|
|||
self._thread: Optional[threading.Thread] = None
|
||||
|
||||
def main(self):
|
||||
"""
|
||||
Implementation of the main loop of the plugin.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def should_stop(self):
|
||||
def should_stop(self) -> bool:
|
||||
return self._should_stop.is_set()
|
||||
|
||||
def wait_stop(self, timeout=None):
|
||||
"""
|
||||
Wait until a stop event is received.
|
||||
"""
|
||||
return self._should_stop.wait(timeout=timeout)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the plugin.
|
||||
"""
|
||||
self._thread = threading.Thread(
|
||||
target=self._runner, name=self.__class__.__name__
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the plugin.
|
||||
"""
|
||||
self._should_stop.set()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self.logger.info('Waiting for the plugin to stop')
|
||||
|
@ -129,6 +142,9 @@ class RunnablePlugin(Plugin):
|
|||
self.logger.info('%s stopped', self.__class__.__name__)
|
||||
|
||||
def _runner(self):
|
||||
"""
|
||||
Implementation of the runner thread.
|
||||
"""
|
||||
self.logger.info('Starting %s', self.__class__.__name__)
|
||||
|
||||
while not self.should_stop():
|
||||
|
@ -151,7 +167,7 @@ class AsyncRunnablePlugin(RunnablePlugin, ABC):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = asyncio.new_event_loop()
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
||||
@property
|
||||
|
@ -185,7 +201,9 @@ class AsyncRunnablePlugin(RunnablePlugin, ABC):
|
|||
raise e
|
||||
|
||||
def _run_listener(self):
|
||||
self._loop = asyncio.new_event_loop()
|
||||
"""
|
||||
Initialize an event loop and run the listener as a task.
|
||||
"""
|
||||
asyncio.set_event_loop(self._loop)
|
||||
|
||||
self._task = self._loop.create_task(self._listen())
|
||||
|
@ -198,6 +216,7 @@ class AsyncRunnablePlugin(RunnablePlugin, ABC):
|
|||
|
||||
self._task.cancel()
|
||||
|
||||
@override
|
||||
def main(self):
|
||||
if self.should_stop():
|
||||
self.logger.info('The plugin is already scheduled to stop')
|
||||
|
@ -214,6 +233,7 @@ class AsyncRunnablePlugin(RunnablePlugin, ABC):
|
|||
else:
|
||||
self.wait_stop()
|
||||
|
||||
@override
|
||||
def stop(self):
|
||||
if self._loop and self._loop.is_running():
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import base64
|
||||
from asyncio import Event, ensure_future
|
||||
from contextlib import asynccontextmanager
|
||||
from threading import RLock
|
||||
from threading import RLock, Timer
|
||||
from typing import AsyncGenerator, Collection, List, Optional, Dict, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
|
@ -10,7 +11,7 @@ from typing_extensions import override
|
|||
|
||||
from platypush.context import get_bus, get_or_create_event_loop
|
||||
from platypush.entities import Entity, EntityManager
|
||||
from platypush.entities.devices import Device
|
||||
from platypush.entities.bluetooth import BluetoothDevice
|
||||
from platypush.message.event.bluetooth.ble import (
|
||||
BluetoothDeviceBlockedEvent,
|
||||
BluetoothDeviceConnectedEvent,
|
||||
|
@ -22,7 +23,9 @@ from platypush.message.event.bluetooth.ble import (
|
|||
BluetoothDeviceUnblockedEvent,
|
||||
BluetoothDeviceUnpairedEvent,
|
||||
BluetoothDeviceUntrustedEvent,
|
||||
BluetoothEvent,
|
||||
BluetoothDeviceEvent,
|
||||
BluetoothScanPausedEvent,
|
||||
BluetoothScanResumedEvent,
|
||||
)
|
||||
from platypush.plugins import AsyncRunnablePlugin, action
|
||||
|
||||
|
@ -52,7 +55,8 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
interface: Optional[str] = None,
|
||||
connect_timeout: float = _default_connect_timeout,
|
||||
device_names: Optional[Dict[str, str]] = None,
|
||||
service_uuids: Optional[Collection[UUIDType]] = None,
|
||||
characteristics: Optional[Collection[UUIDType]] = None,
|
||||
scan_paused_on_start: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
@ -60,7 +64,8 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
on Linux). Default: first available interface.
|
||||
:param connect_timeout: Timeout in seconds for the connection to a
|
||||
Bluetooth device. Default: 5 seconds.
|
||||
:param service_uuids: List of service UUIDs to discover. Default: all.
|
||||
:param characteristics: List of service/characteristic UUIDs to
|
||||
discover. Default: all.
|
||||
:param device_names: Bluetooth address -> device name mapping. If not
|
||||
specified, the device's advertised name will be used, or its
|
||||
Bluetooth address. Example:
|
||||
|
@ -73,13 +78,19 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
"00:11:22:33:44:57": "Button"
|
||||
}
|
||||
|
||||
:param scan_paused_on_start: If ``True``, the plugin will not the
|
||||
scanning thread until :meth:`.scan_resume` is called (default:
|
||||
``False``).
|
||||
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._interface = interface
|
||||
self._connect_timeout = connect_timeout
|
||||
self._service_uuids = service_uuids
|
||||
self._characteristics = characteristics
|
||||
self._scan_lock = RLock()
|
||||
self._scan_enabled = Event()
|
||||
self._scan_controller_timer: Optional[Timer] = None
|
||||
self._connections: Dict[str, BleakClient] = {}
|
||||
self._devices: Dict[str, BLEDevice] = {}
|
||||
self._device_name_by_addr = device_names or {}
|
||||
|
@ -87,6 +98,9 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
name: addr for addr, name in self._device_name_by_addr.items()
|
||||
}
|
||||
|
||||
if not scan_paused_on_start:
|
||||
self._scan_enabled.set()
|
||||
|
||||
async def _get_device(self, device: str) -> BLEDevice:
|
||||
"""
|
||||
Utility method to get a device by name or address.
|
||||
|
@ -113,7 +127,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
)
|
||||
|
||||
def _post_event(
|
||||
self, event_type: Type[BluetoothEvent], device: BLEDevice, **kwargs
|
||||
self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
|
||||
):
|
||||
props = device.details.get('props', {})
|
||||
get_bus().post(
|
||||
|
@ -124,13 +138,13 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
paired=props.get('Paired', False),
|
||||
blocked=props.get('Blocked', False),
|
||||
trusted=props.get('Trusted', False),
|
||||
service_uuids=device.metadata.get('uuids', []),
|
||||
characteristics=device.metadata.get('uuids', []),
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
def _on_device_event(self, device: BLEDevice, _):
|
||||
event_types: List[Type[BluetoothEvent]] = []
|
||||
event_types: List[Type[BluetoothDeviceEvent]] = []
|
||||
existing_device = self._devices.get(device.address)
|
||||
|
||||
if existing_device:
|
||||
|
@ -168,6 +182,9 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
event_types.append(BluetoothDeviceFoundEvent)
|
||||
|
||||
self._devices[device.address] = device
|
||||
if device.name:
|
||||
self._device_name_by_addr[device.address] = device.name
|
||||
self._device_addr_by_name[device.name] = device.address
|
||||
|
||||
if event_types:
|
||||
for event_type in event_types:
|
||||
|
@ -217,7 +234,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
async def _scan(
|
||||
self,
|
||||
duration: Optional[float] = None,
|
||||
service_uuids: Optional[Collection[UUIDType]] = None,
|
||||
characteristics: Optional[Collection[UUIDType]] = None,
|
||||
publish_entities: bool = False,
|
||||
) -> Collection[Entity]:
|
||||
with self._scan_lock:
|
||||
|
@ -226,7 +243,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
adapter=self._interface,
|
||||
timeout=timeout,
|
||||
service_uuids=list(
|
||||
map(str, service_uuids or self._service_uuids or [])
|
||||
map(str, characteristics or self._characteristics or [])
|
||||
),
|
||||
detection_callback=self._on_device_event,
|
||||
)
|
||||
|
@ -234,29 +251,75 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
# TODO Infer type from device.metadata['manufacturer_data']
|
||||
|
||||
self._devices.update({dev.address: dev for dev in devices})
|
||||
if publish_entities:
|
||||
entities = self.publish_entities(devices)
|
||||
else:
|
||||
entities = self.transform_entities(devices)
|
||||
return (
|
||||
self.publish_entities(devices)
|
||||
if publish_entities
|
||||
else self.transform_entities(devices)
|
||||
)
|
||||
|
||||
return entities
|
||||
async def _scan_state_set(self, state: bool, duration: Optional[float] = None):
|
||||
def timer_callback():
|
||||
if state:
|
||||
self.scan_pause()
|
||||
else:
|
||||
self.scan_resume()
|
||||
|
||||
self._scan_controller_timer = None
|
||||
|
||||
with self._scan_lock:
|
||||
if not state and self._scan_enabled.is_set():
|
||||
get_bus().post(BluetoothScanPausedEvent(duration=duration))
|
||||
elif state and not self._scan_enabled.is_set():
|
||||
get_bus().post(BluetoothScanResumedEvent(duration=duration))
|
||||
|
||||
if state:
|
||||
self._scan_enabled.set()
|
||||
else:
|
||||
self._scan_enabled.clear()
|
||||
|
||||
if duration and not self._scan_controller_timer:
|
||||
self._scan_controller_timer = Timer(duration, timer_callback)
|
||||
self._scan_controller_timer.start()
|
||||
|
||||
@action
|
||||
def scan_pause(self, duration: Optional[float] = None):
|
||||
"""
|
||||
Pause the scanning thread.
|
||||
|
||||
:param duration: For how long the scanning thread should be paused
|
||||
(default: null = indefinitely).
|
||||
"""
|
||||
if self._loop:
|
||||
ensure_future(self._scan_state_set(False, duration), loop=self._loop)
|
||||
|
||||
@action
|
||||
def scan_resume(self, duration: Optional[float] = None):
|
||||
"""
|
||||
Resume the scanning thread, if inactive.
|
||||
|
||||
:param duration: For how long the scanning thread should be running
|
||||
(default: null = indefinitely).
|
||||
"""
|
||||
if self._loop:
|
||||
ensure_future(self._scan_state_set(True, duration), loop=self._loop)
|
||||
|
||||
@action
|
||||
def scan(
|
||||
self,
|
||||
duration: Optional[float] = None,
|
||||
service_uuids: Optional[Collection[UUIDType]] = None,
|
||||
characteristics: Optional[Collection[UUIDType]] = None,
|
||||
):
|
||||
"""
|
||||
Scan for Bluetooth devices nearby.
|
||||
Scan for Bluetooth devices nearby and return the results as a list of
|
||||
entities.
|
||||
|
||||
:param duration: Scan duration in seconds (default: same as the plugin's
|
||||
`poll_interval` configuration parameter)
|
||||
:param service_uuids: List of service UUIDs to discover. Default: all.
|
||||
:param characteristics: List of characteristic UUIDs to discover. Default: all.
|
||||
"""
|
||||
loop = get_or_create_event_loop()
|
||||
loop.run_until_complete(
|
||||
self._scan(duration, service_uuids, publish_entities=True)
|
||||
return loop.run_until_complete(
|
||||
self._scan(duration, characteristics, publish_entities=True)
|
||||
)
|
||||
|
||||
@action
|
||||
|
@ -319,9 +382,11 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
return self.scan().output
|
||||
|
||||
@override
|
||||
def transform_entities(self, entities: Collection[BLEDevice]) -> Collection[Device]:
|
||||
def transform_entities(
|
||||
self, entities: Collection[BLEDevice]
|
||||
) -> Collection[BluetoothDevice]:
|
||||
return [
|
||||
Device(
|
||||
BluetoothDevice(
|
||||
id=dev.address,
|
||||
name=self._get_device_name(dev),
|
||||
)
|
||||
|
@ -333,7 +398,9 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
device_addresses = set()
|
||||
|
||||
while True:
|
||||
await self._scan_enabled.wait()
|
||||
entities = await self._scan()
|
||||
|
||||
new_device_addresses = {e.id for e in entities}
|
||||
missing_device_addresses = device_addresses - new_device_addresses
|
||||
missing_devices = [
|
||||
|
@ -348,5 +415,12 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
|||
|
||||
device_addresses = new_device_addresses
|
||||
|
||||
@override
|
||||
def stop(self):
|
||||
if self._scan_controller_timer:
|
||||
self._scan_controller_timer.cancel()
|
||||
|
||||
super().stop()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -126,7 +126,7 @@ class MqttPlugin(Plugin):
|
|||
if version == 'tlsv1.2':
|
||||
return ssl.PROTOCOL_TLSv1_2
|
||||
|
||||
assert 'Unrecognized TLS version: {}'.format(version)
|
||||
assert f'Unrecognized TLS version: {version}'
|
||||
|
||||
def _mqtt_args(self, **kwargs):
|
||||
return {
|
||||
|
|
|
@ -356,7 +356,7 @@ class SmartthingsPlugin(
|
|||
}
|
||||
|
||||
missing_devs = {dev for dev in devices if dev not in found_devs}
|
||||
return list(found_devs.values()), list(missing_devs) # type: ignore
|
||||
return list(found_devs.values()), list(missing_devs)
|
||||
|
||||
def _get_devices(self, *devices: str) -> List[DeviceEntity]:
|
||||
devs, missing_devs = self._get_existing_and_missing_devices(*devices)
|
||||
|
@ -633,7 +633,7 @@ class SmartthingsPlugin(
|
|||
|
||||
self._entities_by_id.update({e.id: e for e in compatible_entities})
|
||||
|
||||
return super().transform_entities(compatible_entities) # type: ignore
|
||||
return super().transform_entities(compatible_entities)
|
||||
|
||||
async def _get_device_status(
|
||||
self, api, device_id: str, publish_entities: bool
|
||||
|
@ -642,7 +642,7 @@ class SmartthingsPlugin(
|
|||
assert device, f'No such device: {device_id}'
|
||||
await device.status.refresh()
|
||||
if publish_entities:
|
||||
self.publish_entities([device]) # type: ignore
|
||||
self.publish_entities([device])
|
||||
|
||||
self._devices_by_id[device_id] = device
|
||||
self._devices_by_name[device.label] = device
|
||||
|
@ -863,7 +863,6 @@ class SmartthingsPlugin(
|
|||
|
||||
@action
|
||||
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
|
||||
super().set(entity, value, attribute, **kwargs)
|
||||
return self.set_value(entity, property=attribute, value=value, **kwargs)
|
||||
|
||||
@action
|
||||
|
@ -994,6 +993,7 @@ class SmartthingsPlugin(
|
|||
self.logger.exception(e)
|
||||
self.logger.error('Could not refresh the status: %s', e)
|
||||
self.wait_stop(3 * (self.poll_interval or 5))
|
||||
return None
|
||||
|
||||
while not self.should_stop():
|
||||
updated_devices = {}
|
||||
|
@ -1010,7 +1010,7 @@ class SmartthingsPlugin(
|
|||
if self._has_status_changed(devices.get(device_id, {}), new_status)
|
||||
}
|
||||
|
||||
self.publish_entities(updated_devices.values()) # type: ignore
|
||||
self.publish_entities(updated_devices.values())
|
||||
devices.update(new_devices)
|
||||
self.wait_stop(self.poll_interval)
|
||||
refresh_status_safe()
|
||||
|
|
|
@ -34,7 +34,7 @@ class DeviceMapper:
|
|||
entity_type: Type[Entity],
|
||||
capability: str,
|
||||
attribute: str,
|
||||
value_type: Union[Type, str],
|
||||
value_type: Union[Type, Enum, str],
|
||||
set_command: Optional[Union[str, Callable[[Any], str]]] = None,
|
||||
get_value: Optional[Callable[[DeviceEntity], Any]] = None,
|
||||
set_value_args: Optional[Callable[..., Any]] = None,
|
||||
|
@ -46,7 +46,7 @@ class DeviceMapper:
|
|||
self.attribute = attribute
|
||||
self.value_type = value_type
|
||||
self.get_value = get_value if get_value else self._default_get_value
|
||||
self.values = []
|
||||
self.values: List[str] = []
|
||||
self.entity_args = kwargs
|
||||
|
||||
if isinstance(value_type, Enum):
|
||||
|
|
|
@ -54,7 +54,7 @@ class SwitchbotBluetoothPlugin(BluetoothBlePlugin, EnumSwitchEntityManager):
|
|||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, service_uuids=self._uuids.values(), **kwargs)
|
||||
super().__init__(*args, characteristics=self._uuids.values(), **kwargs)
|
||||
|
||||
async def _run(
|
||||
self,
|
||||
|
@ -122,7 +122,7 @@ class SwitchbotBluetoothPlugin(BluetoothBlePlugin, EnumSwitchEntityManager):
|
|||
self.logger.warning('Unknown command for SwitchBot "%s": "%s"', device, value)
|
||||
|
||||
@override
|
||||
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
|
||||
def set(self, entity: str, value: Any, **kwargs):
|
||||
return self.set_value(entity, value, **kwargs)
|
||||
|
||||
@override
|
||||
|
|
|
@ -348,7 +348,7 @@ class ZwaveBasePlugin(
|
|||
raise NotImplementedError
|
||||
|
||||
@action
|
||||
def set(self, entity: str, value: Any, attribute: Optional[str] = None, **kwargs):
|
||||
def set(self, entity: str, value: Any, **kwargs):
|
||||
return self.set_value(
|
||||
value_id=entity, id_on_network=entity, data=value, **kwargs
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue