Added bluetooth plugin (see #89)
This commit is contained in:
parent
2c8993e67d
commit
8c41110145
7 changed files with 554 additions and 28 deletions
|
@ -21,6 +21,12 @@ include:
|
||||||
- include/media.yaml
|
- include/media.yaml
|
||||||
- include/sensors.yaml
|
- include/sensors.yaml
|
||||||
|
|
||||||
|
# platypush logs on stdout by default. You can use the logging section to specify
|
||||||
|
# an alternative file or change the logging level.
|
||||||
|
logging:
|
||||||
|
filename: ~/.local/log/platypush/platypush.log
|
||||||
|
level: INFO
|
||||||
|
|
||||||
# The device_id is used by many components of platypush and it should uniquely
|
# The device_id is used by many components of platypush and it should uniquely
|
||||||
# identify a device in your network. If nothing is specified then the hostname
|
# identify a device in your network. If nothing is specified then the hostname
|
||||||
# will be used.
|
# will be used.
|
||||||
|
|
|
@ -9,7 +9,6 @@ from platypush.backend.http.app.utils import authenticate, get_websocket_port
|
||||||
from platypush.backend.http.utils import HttpUtils
|
from platypush.backend.http.utils import HttpUtils
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
|
|
||||||
|
|
||||||
index = Blueprint('index', __name__, template_folder=template_folder)
|
index = Blueprint('index', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
# Declare routes list
|
# Declare routes list
|
||||||
|
|
|
@ -3,10 +3,11 @@ import time
|
||||||
|
|
||||||
from platypush.message import Message
|
from platypush.message import Message
|
||||||
|
|
||||||
|
|
||||||
class Response(Message):
|
class Response(Message):
|
||||||
""" Response message class """
|
""" Response message class """
|
||||||
|
|
||||||
def __init__(self, target=None, origin=None, id=None, output=None, errors=[],
|
def __init__(self, target=None, origin=None, id=None, output=None, errors=None,
|
||||||
timestamp=None, disable_logging=False):
|
timestamp=None, disable_logging=False):
|
||||||
"""
|
"""
|
||||||
Params:
|
Params:
|
||||||
|
@ -21,13 +22,13 @@ class Response(Message):
|
||||||
super().__init__(timestamp=timestamp)
|
super().__init__(timestamp=timestamp)
|
||||||
self.target = target
|
self.target = target
|
||||||
self.output = self._parse_msg(output)
|
self.output = self._parse_msg(output)
|
||||||
self.errors = self._parse_msg(errors)
|
self.errors = self._parse_msg(errors or [])
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.id = id
|
self.id = id
|
||||||
self.disable_logging = disable_logging
|
self.disable_logging = disable_logging
|
||||||
|
|
||||||
def is_error(self):
|
def is_error(self):
|
||||||
""" Returns True if the respopnse has errors """
|
""" Returns True if the response has errors """
|
||||||
return len(self.errors) != 0
|
return len(self.errors) != 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -35,12 +36,13 @@ class Response(Message):
|
||||||
if isinstance(msg, bytes) or isinstance(msg, bytearray):
|
if isinstance(msg, bytes) or isinstance(msg, bytearray):
|
||||||
msg = msg.decode('utf-8')
|
msg = msg.decode('utf-8')
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
try: msg = json.loads(msg.strip())
|
try:
|
||||||
except ValueError as e: pass
|
msg = json.loads(msg.strip())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, msg):
|
def build(cls, msg):
|
||||||
msg = super().parse(msg)
|
msg = super().parse(msg)
|
||||||
|
@ -48,14 +50,16 @@ class Response(Message):
|
||||||
'target': msg['target'],
|
'target': msg['target'],
|
||||||
'output': msg['response']['output'],
|
'output': msg['response']['output'],
|
||||||
'errors': msg['response']['errors'],
|
'errors': msg['response']['errors'],
|
||||||
|
'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time(),
|
||||||
|
'disable_logging': msg.get('_disable_logging', False),
|
||||||
}
|
}
|
||||||
|
|
||||||
args['timestamp'] = msg['_timestamp'] if '_timestamp' in msg else time.time()
|
if 'id' in msg:
|
||||||
args['disable_logging'] = msg.get('_disable_logging', False)
|
args['id'] = msg['id']
|
||||||
if 'id' in msg: args['id'] = msg['id']
|
if 'origin' in msg:
|
||||||
if 'origin' in msg: args['origin'] = msg['origin']
|
args['origin'] = msg['origin']
|
||||||
return cls(**args)
|
|
||||||
|
|
||||||
|
return cls(**args)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -80,5 +84,4 @@ class Response(Message):
|
||||||
|
|
||||||
return json.dumps(response_dict)
|
return json.dumps(response_dict)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
101
platypush/message/response/bluetooth.py
Normal file
101
platypush/message/response/bluetooth.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
from platypush.message.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothResponse(Response):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothScanResponse(BluetoothResponse):
|
||||||
|
def __init__(self, devices, *args, **kwargs):
|
||||||
|
if isinstance(devices, list):
|
||||||
|
self.devices = [
|
||||||
|
{
|
||||||
|
'addr': dev[0],
|
||||||
|
'name': dev[1] if len(dev) > 1 else None,
|
||||||
|
'class': hex(dev[2]) if len(dev) > 2 else None,
|
||||||
|
}
|
||||||
|
for dev in devices
|
||||||
|
]
|
||||||
|
elif isinstance(devices, dict):
|
||||||
|
self.devices = [
|
||||||
|
{
|
||||||
|
'addr': addr,
|
||||||
|
'name': name or None,
|
||||||
|
'class': 'BLE',
|
||||||
|
}
|
||||||
|
for addr, name in devices.items()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise ValueError('devices must be either a list of tuples or a dict')
|
||||||
|
|
||||||
|
super().__init__(output=self.devices, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothLookupNameResponse(BluetoothResponse):
|
||||||
|
def __init__(self, addr: str, name: str, *args, **kwargs):
|
||||||
|
self.addr = addr
|
||||||
|
self.name = name
|
||||||
|
super().__init__(output={
|
||||||
|
'addr': self.addr,
|
||||||
|
'name': self.name
|
||||||
|
}, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothLookupServiceResponse(BluetoothResponse):
|
||||||
|
"""
|
||||||
|
Example services response output::
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"service-classes": [
|
||||||
|
"1801"
|
||||||
|
],
|
||||||
|
"profiles": [],
|
||||||
|
"name": "Service name #1",
|
||||||
|
"description": null,
|
||||||
|
"provider": null,
|
||||||
|
"service-id": null,
|
||||||
|
"protocol": "L2CAP",
|
||||||
|
"port": 31,
|
||||||
|
"host": "00:11:22:33:44:55"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service-classes": [
|
||||||
|
"1800"
|
||||||
|
],
|
||||||
|
"profiles": [],
|
||||||
|
"name": "Service name #2",
|
||||||
|
"description": null,
|
||||||
|
"provider": null,
|
||||||
|
"service-id": null,
|
||||||
|
"protocol": "L2CAP",
|
||||||
|
"port": 31,
|
||||||
|
"host": "00:11:22:33:44:56"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service-classes": [
|
||||||
|
"1112",
|
||||||
|
"1203"
|
||||||
|
],
|
||||||
|
"profiles": [
|
||||||
|
[
|
||||||
|
"1108",
|
||||||
|
258
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "Headset Gateway",
|
||||||
|
"description": null,
|
||||||
|
"provider": null,
|
||||||
|
"service-id": null,
|
||||||
|
"protocol": "RFCOMM",
|
||||||
|
"port": 2,
|
||||||
|
"host": "00:11:22:33:44:57"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
def __init__(self, services: list, *args, **kwargs):
|
||||||
|
self.services = services
|
||||||
|
super().__init__(output=self.services, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
415
platypush/plugins/bluetooth/__init__.py
Normal file
415
platypush/plugins/bluetooth/__init__.py
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import select
|
||||||
|
|
||||||
|
from platypush.plugins import Plugin, action
|
||||||
|
from platypush.message.response.bluetooth import BluetoothScanResponse, \
|
||||||
|
BluetoothLookupNameResponse, BluetoothLookupServiceResponse, BluetoothResponse
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothPlugin(Plugin):
|
||||||
|
"""
|
||||||
|
Bluetooth plugin
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* **pybluez** (``pip install pybluez``)
|
||||||
|
* **pyobex** (``pip install pyobex``) [optional] for file transfer support
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bluetooth
|
||||||
|
|
||||||
|
class _DeviceDiscoverer(bluetooth.DeviceDiscoverer):
|
||||||
|
def __init__(self, name, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.name = name
|
||||||
|
self.device = {}
|
||||||
|
self.done = True
|
||||||
|
|
||||||
|
def pre_inquiry(self):
|
||||||
|
self.done = False
|
||||||
|
|
||||||
|
def device_discovered(self, dev_addr, dev_class, rssi, dev_name):
|
||||||
|
dev_name = dev_name.decode()
|
||||||
|
if dev_name == self.name:
|
||||||
|
self.device = {
|
||||||
|
'addr': dev_addr,
|
||||||
|
'name': dev_name,
|
||||||
|
'class': dev_class,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.done = True
|
||||||
|
|
||||||
|
def inquiry_complete(self):
|
||||||
|
self.done = True
|
||||||
|
|
||||||
|
def __init__(self, device_id: int = -1, **kwargs):
|
||||||
|
"""
|
||||||
|
:param device_id: Default adapter device_id to be used (default: -1, auto)
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.device_id = device_id
|
||||||
|
self._devices = []
|
||||||
|
self._devices_by_addr = {}
|
||||||
|
self._devices_by_name = {}
|
||||||
|
self._port_and_protocol_by_addr_and_srv_uuid = {}
|
||||||
|
self._port_and_protocol_by_addr_and_srv_name = {}
|
||||||
|
self._socks = {}
|
||||||
|
|
||||||
|
def _get_device_addr(self, device):
|
||||||
|
if re.match('([0-9A-F]{2}:){5}[0-9A-F]{2}', device, re.IGNORECASE):
|
||||||
|
return device
|
||||||
|
if device in self._devices_by_name:
|
||||||
|
return self._devices_by_name[device]['addr']
|
||||||
|
|
||||||
|
return self.lookup_address(device).output['addr']
|
||||||
|
|
||||||
|
@action
|
||||||
|
def scan(self, device_id: int = None, duration: int = 10) -> BluetoothScanResponse:
|
||||||
|
"""
|
||||||
|
Scan for nearby bluetooth devices
|
||||||
|
|
||||||
|
:param device_id: Bluetooth adapter ID to use (default configured if None)
|
||||||
|
:param duration: Scan duration in seconds
|
||||||
|
"""
|
||||||
|
from bluetooth import discover_devices
|
||||||
|
|
||||||
|
if device_id is None:
|
||||||
|
device_id = self.device_id
|
||||||
|
|
||||||
|
self.logger.info('Discovering devices on adapter {}, duration: {} seconds'.format(
|
||||||
|
device_id, duration))
|
||||||
|
|
||||||
|
devices = discover_devices(duration=duration, lookup_names=True, lookup_class=True, device_id=device_id,
|
||||||
|
flush_cache=True)
|
||||||
|
response = BluetoothScanResponse(devices)
|
||||||
|
|
||||||
|
self._devices = response.devices
|
||||||
|
self._devices_by_addr = {dev['addr']: dev for dev in self._devices}
|
||||||
|
self._devices_by_name = {dev['name']: dev for dev in self._devices if dev.get('name')}
|
||||||
|
return response
|
||||||
|
|
||||||
|
@action
|
||||||
|
def lookup_name(self, addr: str, timeout: int = 10) -> BluetoothLookupNameResponse:
|
||||||
|
"""
|
||||||
|
Look up the name of a nearby bluetooth device given the address
|
||||||
|
|
||||||
|
:param addr: Device address
|
||||||
|
:param timeout: Lookup timeout (default: 10 seconds)
|
||||||
|
"""
|
||||||
|
from bluetooth import lookup_name
|
||||||
|
|
||||||
|
self.logger.info('Looking up name for device {}'.format(addr))
|
||||||
|
name = lookup_name(addr, timeout=timeout)
|
||||||
|
|
||||||
|
dev = {
|
||||||
|
'addr': addr,
|
||||||
|
'name': name,
|
||||||
|
'class': self._devices_by_addr.get(addr, {}).get('class'),
|
||||||
|
}
|
||||||
|
|
||||||
|
self._devices_by_addr[addr] = dev
|
||||||
|
if name:
|
||||||
|
self._devices_by_name[name] = dev
|
||||||
|
|
||||||
|
return BluetoothLookupNameResponse(addr=addr, name=name)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def lookup_address(self, name: str, timeout: int = 10) -> BluetoothLookupNameResponse:
|
||||||
|
"""
|
||||||
|
Look up the address of a nearby bluetooth device given the name
|
||||||
|
|
||||||
|
:param name: Device name
|
||||||
|
:param timeout: Lookup timeout (default: 10 seconds)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.logger.info('Looking up address for device {}'.format(name))
|
||||||
|
discoverer = self._DeviceDiscoverer(name)
|
||||||
|
discoverer.find_devices(lookup_names=True, duration=timeout)
|
||||||
|
readfiles = [discoverer]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
rfds = select.select(readfiles, [], [])[0]
|
||||||
|
if discoverer in rfds:
|
||||||
|
discoverer.process_event()
|
||||||
|
|
||||||
|
if discoverer.done:
|
||||||
|
break
|
||||||
|
|
||||||
|
dev = discoverer.device
|
||||||
|
if not dev:
|
||||||
|
raise RuntimeError('No such device: {}'.format(name))
|
||||||
|
|
||||||
|
addr = dev.get('addr')
|
||||||
|
self._devices_by_addr[addr] = dev
|
||||||
|
self._devices_by_name[name] = dev
|
||||||
|
return BluetoothLookupNameResponse(addr=addr, name=name)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def find_service(self, name: str = None, addr: str = None, uuid: str = None) -> BluetoothLookupServiceResponse:
|
||||||
|
"""
|
||||||
|
Look up for a service published by a nearby bluetooth device. If all the parameters are null then all the
|
||||||
|
published services on the nearby devices will be returned. See
|
||||||
|
`:class:platypush.message.response.bluetoothBluetoothLookupServiceResponse` for response structure reference.
|
||||||
|
|
||||||
|
:param name: Service name
|
||||||
|
:param addr: Service/device address
|
||||||
|
:param uuid: Service UUID
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bluetooth
|
||||||
|
from bluetooth import find_service
|
||||||
|
services = find_service(name=name, address=addr, uuid=uuid)
|
||||||
|
|
||||||
|
self._port_and_protocol_by_addr_and_srv_uuid.update({
|
||||||
|
(srv['host'], srv['service-id']): (srv['port'], getattr(bluetooth, srv['protocol']))
|
||||||
|
for srv in services if srv.get('service-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
self._port_and_protocol_by_addr_and_srv_name.update({
|
||||||
|
(srv['host'], srv['name']): (srv['port'], getattr(bluetooth, srv['protocol']))
|
||||||
|
for srv in services if srv.get('name')
|
||||||
|
})
|
||||||
|
|
||||||
|
return BluetoothLookupServiceResponse(services)
|
||||||
|
|
||||||
|
def _get_sock(self, protocol=None, device: str = None, port: int = None, service_uuid: str = None,
|
||||||
|
service_name: str = None, connect_if_closed=False):
|
||||||
|
sock = None
|
||||||
|
addr = self._get_device_addr(device)
|
||||||
|
|
||||||
|
if not (addr and port and protocol):
|
||||||
|
addr, port, protocol = self._get_addr_port_protocol(protocol=protocol, device=device, port=port,
|
||||||
|
service_uuid=service_uuid, service_name=service_name)
|
||||||
|
|
||||||
|
if (addr, port) in self._socks:
|
||||||
|
sock = self._socks[(addr, port)]
|
||||||
|
elif connect_if_closed:
|
||||||
|
self.connect(protocol=protocol, device=device, port=port, service_uuid=service_uuid,
|
||||||
|
service_name=service_name)
|
||||||
|
sock = self._socks[(addr, port)]
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
def _get_addr_port_protocol(self, protocol=None, device: str = None, port: int = None, service_uuid: str = None,
|
||||||
|
service_name: str = None) -> tuple:
|
||||||
|
import bluetooth
|
||||||
|
|
||||||
|
addr = self._get_device_addr(device) if device else None
|
||||||
|
if service_uuid or service_name:
|
||||||
|
if addr:
|
||||||
|
if service_uuid:
|
||||||
|
(port, protocol) = self._port_and_protocol_by_addr_and_srv_uuid[(addr, service_uuid)] \
|
||||||
|
if (addr, service_uuid) in self._port_and_protocol_by_addr_and_srv_uuid else \
|
||||||
|
(None, None)
|
||||||
|
else:
|
||||||
|
(port, protocol) = self._port_and_protocol_by_addr_and_srv_name[(addr, service_name)] \
|
||||||
|
if (addr, service_name) in self._port_and_protocol_by_addr_and_srv_name else \
|
||||||
|
(None, None)
|
||||||
|
|
||||||
|
if not (addr and port):
|
||||||
|
self.logger.info('Discovering devices, service_name={name}, uuid={uuid}, address={addr}'.format(
|
||||||
|
name=service_name, uuid=service_uuid, addr=addr))
|
||||||
|
|
||||||
|
services = [
|
||||||
|
srv for srv in self.find_service().services
|
||||||
|
if (service_name is None or srv.get('name') == service_name) and
|
||||||
|
(addr is None or srv.get('host') == addr) and
|
||||||
|
(service_uuid is None or srv.get('service-id') == service_uuid)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not services:
|
||||||
|
raise RuntimeError('No such service: name={name} uuid={uuid} address={addr}'.format(
|
||||||
|
name=service_name, uuid=service_uuid, addr=addr))
|
||||||
|
|
||||||
|
service = services[0]
|
||||||
|
addr = service['host']
|
||||||
|
port = service['port']
|
||||||
|
protocol = getattr(bluetooth, service['protocol'])
|
||||||
|
elif protocol:
|
||||||
|
if isinstance(protocol, str):
|
||||||
|
protocol = getattr(bluetooth, protocol)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('No service name/UUID nor bluetooth protocol (RFCOMM/L2CAP) specified')
|
||||||
|
|
||||||
|
if not (addr and port):
|
||||||
|
raise RuntimeError('No valid device name/address, port, service name or UUID specified')
|
||||||
|
|
||||||
|
return addr, port, protocol
|
||||||
|
|
||||||
|
@action
|
||||||
|
def connect(self, protocol=None, device: str = None, port: int = None, service_uuid: str = None,
|
||||||
|
service_name: str = None):
|
||||||
|
"""
|
||||||
|
Connect to a bluetooth device.
|
||||||
|
You can query the advertised services through ``find_service``.
|
||||||
|
|
||||||
|
:param protocol: Supported values: either 'RFCOMM'/'L2CAP' (str) or bluetooth.RFCOMM/bluetooth.L2CAP
|
||||||
|
int constants (int)
|
||||||
|
:param device: Device address or name
|
||||||
|
:param port: Port number
|
||||||
|
:param service_uuid: Service UUID
|
||||||
|
:param service_name: Service name
|
||||||
|
"""
|
||||||
|
from bluetooth import BluetoothSocket
|
||||||
|
|
||||||
|
addr, port, protocol = self._get_addr_port_protocol(protocol=protocol, device=device, port=port,
|
||||||
|
service_uuid=service_uuid, service_name=service_name)
|
||||||
|
sock = self._get_sock(protocol=protocol, device=addr, port=port)
|
||||||
|
if sock:
|
||||||
|
self.close(device=addr, port=port)
|
||||||
|
|
||||||
|
sock = BluetoothSocket(protocol)
|
||||||
|
self.logger.info('Opening connection to device {} on port {}'.format(addr, port))
|
||||||
|
sock.connect((addr, port))
|
||||||
|
self.logger.info('Connected to device {} on port {}'.format(addr, port))
|
||||||
|
self._socks[(addr, port)] = sock
|
||||||
|
|
||||||
|
@action
|
||||||
|
def close(self, device: str = None, port: int = None, service_uuid: str = None, service_name: str = None):
|
||||||
|
"""
|
||||||
|
Close an active bluetooth connection
|
||||||
|
|
||||||
|
:param device: Device address or name
|
||||||
|
:param port: Port number
|
||||||
|
:param service_uuid: Service UUID
|
||||||
|
:param service_name: Service name
|
||||||
|
"""
|
||||||
|
sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
self.logger.info('Close on device {}({}) that is not connected'.format(device, port))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Exception while closing previous connection to {}({}): {}'.format(
|
||||||
|
device, port, str(e)))
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send(self, data, device: str = None, port: int = None, service_uuid: str = None, service_name: str = None,
|
||||||
|
binary: bool = False):
|
||||||
|
"""
|
||||||
|
Send data to an active bluetooth connection
|
||||||
|
|
||||||
|
:param data: Data to be sent
|
||||||
|
:param device: Device address or name
|
||||||
|
:param service_uuid: Service UUID
|
||||||
|
:param service_name: Service name
|
||||||
|
:param port: Port number
|
||||||
|
:param binary: Set to true if msg is a base64-encoded binary string
|
||||||
|
"""
|
||||||
|
from bluetooth import BluetoothError
|
||||||
|
|
||||||
|
sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name,
|
||||||
|
connect_if_closed=True)
|
||||||
|
|
||||||
|
if binary:
|
||||||
|
data = base64.decodebytes(data.encode() if isinstance(data, str) else data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.send(data)
|
||||||
|
except BluetoothError as e:
|
||||||
|
self.close(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@action
|
||||||
|
def recv(self, device: str, port: int, service_uuid: str = None, service_name: str = None, size: int = 1024,
|
||||||
|
binary: bool = False) -> BluetoothResponse:
|
||||||
|
"""
|
||||||
|
Send data to an active bluetooth connection
|
||||||
|
|
||||||
|
:param device: Device address or name
|
||||||
|
:param port: Port number
|
||||||
|
:param service_uuid: Service UUID
|
||||||
|
:param service_name: Service name
|
||||||
|
:param size: Maximum number of bytes to be read
|
||||||
|
:param binary: Set to true to return a base64-encoded binary string
|
||||||
|
"""
|
||||||
|
from bluetooth import BluetoothError
|
||||||
|
|
||||||
|
sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name,
|
||||||
|
connect_if_closed=True)
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
self.connect(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
|
||||||
|
sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sock.recv(size)
|
||||||
|
except BluetoothError as e:
|
||||||
|
self.close(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if binary:
|
||||||
|
data = base64.encodebytes(data)
|
||||||
|
|
||||||
|
return BluetoothResponse(output=data.decode())
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_l2cap_mtu(self, mtu: int, device: str = None, port: int = None, service_name: str = None,
|
||||||
|
service_uuid: str = None):
|
||||||
|
"""
|
||||||
|
Set the L2CAP MTU (Maximum Transmission Unit) value for a connected bluetooth device.
|
||||||
|
Both the devices usually use the same MTU value over a connection.
|
||||||
|
|
||||||
|
:param device: Device address or name
|
||||||
|
:param port: Port number
|
||||||
|
:param service_uuid: Service UUID
|
||||||
|
:param service_name: Service name
|
||||||
|
:param mtu: New MTU value
|
||||||
|
"""
|
||||||
|
from bluetooth import BluetoothError, set_l2cap_mtu, L2CAP
|
||||||
|
|
||||||
|
sock = self._get_sock(protocol=L2CAP, device=device, port=port, service_uuid=service_uuid,
|
||||||
|
service_name=service_name, connect_if_closed=True)
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
raise RuntimeError('set_l2cap_mtu: device not connected')
|
||||||
|
|
||||||
|
try:
|
||||||
|
set_l2cap_mtu(sock, mtu)
|
||||||
|
except BluetoothError as e:
|
||||||
|
self.close(device=device, port=port, service_name=service_name, service_uuid=service_uuid)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send_file(self, filename: str, device: str, port: int = None, data=None, service_name='OBEX Object Push',
|
||||||
|
binary: bool = False):
|
||||||
|
"""
|
||||||
|
Send a local file to a device that exposes an OBEX Object Push service
|
||||||
|
|
||||||
|
:param filename: Path of the file to be sent
|
||||||
|
:param data: Alternatively to a file on disk you can send raw (string or binary) content
|
||||||
|
:param device: Device address or name
|
||||||
|
:param port: Port number
|
||||||
|
:param service_name: Service name
|
||||||
|
:param binary: Set to true if data is a base64-encoded binary string
|
||||||
|
"""
|
||||||
|
from PyOBEX.client import Client
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
filename = os.path.abspath(os.path.expanduser(filename))
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
data = f.read()
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
else:
|
||||||
|
if binary:
|
||||||
|
data = base64.decodebytes(data.encode() if isinstance(data, str) else data)
|
||||||
|
|
||||||
|
addr, port, protocol = self._get_addr_port_protocol(device=device, port=port,
|
||||||
|
service_name=service_name)
|
||||||
|
|
||||||
|
client = Client(addr, port)
|
||||||
|
self.logger.info('Connecting to device {}'.format(addr))
|
||||||
|
client.connect()
|
||||||
|
self.logger.info('Sending file {} to device {}'.format(filename, addr))
|
||||||
|
client.put(filename, data)
|
||||||
|
self.logger.info('File {} sent to device {}'.format(filename, addr))
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -103,9 +103,10 @@ paho-mqtt
|
||||||
# Serial port support
|
# Serial port support
|
||||||
# pyserial
|
# pyserial
|
||||||
|
|
||||||
# Switchbot devices support
|
# Bluetooth devices support
|
||||||
# pybluez
|
# pybluez
|
||||||
# gattlib
|
# gattlib
|
||||||
|
# git+https://github.com/BlackLight/PyOBEX
|
||||||
|
|
||||||
# Support for TP-Link HS100 and similar smart switches/plugins
|
# Support for TP-Link HS100 and similar smart switches/plugins
|
||||||
# pyHS100
|
# pyHS100
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -149,7 +149,7 @@ setup(
|
||||||
# Support for Kafka backend and plugin
|
# Support for Kafka backend and plugin
|
||||||
'kafka': ['kafka-python'],
|
'kafka': ['kafka-python'],
|
||||||
# Support for Pushbullet backend and plugin
|
# Support for Pushbullet backend and plugin
|
||||||
'pushbullet': ['pushbullet.py'],
|
'pushbullet': ['pushbullet.py @ https://github.com/rbrcsk/pushbullet.py'],
|
||||||
# Support for HTTP backend
|
# Support for HTTP backend
|
||||||
'http': ['flask', 'websockets', 'python-dateutil', 'tz', 'frozendict', 'bcrypt'],
|
'http': ['flask', 'websockets', 'python-dateutil', 'tz', 'frozendict', 'bcrypt'],
|
||||||
# Support for uWSGI HTTP backend
|
# Support for uWSGI HTTP backend
|
||||||
|
@ -237,8 +237,9 @@ setup(
|
||||||
'flic': ['flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'],
|
'flic': ['flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'],
|
||||||
# Support for Alexa/Echo plugin
|
# Support for Alexa/Echo plugin
|
||||||
'alexa': ['avs @ https://github.com:BlackLight/avs/tarball/master'],
|
'alexa': ['avs @ https://github.com:BlackLight/avs/tarball/master'],
|
||||||
# Support for bluetooth and Switchbot plugin
|
# Support for bluetooth devices
|
||||||
'bluetooth': ['pybluez', 'gattlib'],
|
'bluetooth': ['pybluez', 'gattlib',
|
||||||
|
'pyobex @ https://github.com/BlackLight/PyOBEX'],
|
||||||
# Support for TP-Link devices
|
# Support for TP-Link devices
|
||||||
'tplink': ['pyHS100'],
|
'tplink': ['pyHS100'],
|
||||||
# Support for PWM3901 2-Dimensional Optical Flow Sensor
|
# Support for PWM3901 2-Dimensional Optical Flow Sensor
|
||||||
|
|
Loading…
Reference in a new issue