Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
31017b3ade |
460 changed files with 5527 additions and 25136 deletions
104
CHANGELOG.md
104
CHANGELOG.md
|
@ -5,104 +5,6 @@ Given the high speed of development in the first phase, changes are being report
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Added `music.spotify.connect` backend to emulate a Spotify Connect receiver through Platypush.
|
||||
|
||||
## [0.21.1] - 2021-06-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added `switchbot` plugin to interact with Switchbot devices over the cloud API instead of
|
||||
directly accessing the device's Bluetooth interface.
|
||||
|
||||
- Added `marshmallow` dependency - it will be used from now own to dump and document schemas
|
||||
and responses instead of the currently mixed approach with `Response` objects and plain
|
||||
dictionaries and lists.
|
||||
|
||||
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
|
||||
|
||||
- Added generic joystick backend `backend.joystick.jstest` which uses `jstest` from the
|
||||
standard `joystick` system package to read the state of joysticks not compatible with
|
||||
`python-inputs`.
|
||||
|
||||
- Added PWM PCA9685 plugin.
|
||||
|
||||
- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where
|
||||
``python-inputs`` doesn't work and ``jstest`` is too slow.
|
||||
|
||||
### Changed
|
||||
|
||||
- `switch.switchbot` plugin renamed to `switchbot.bluetooth` plugin, while the new plugin
|
||||
that uses the Switchbot API is simply named `switchbot`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- More robust reconnection logic on the Pushbullet backend in case of websocket errors.
|
||||
|
||||
## [0.21.0] - 2021-05-06
|
||||
|
||||
### Added
|
||||
|
||||
- Support for custom PopcornTime API mirror/base URL.
|
||||
|
||||
- Full support for TV series search.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed torrent search (now using a different PopcornTime API mirror).
|
||||
|
||||
- Migrated SASS engine from `node-sass` (currently deprecated and broken on Node 16) to `sass`.
|
||||
|
||||
- Fixed alignment of Z-Wave UI header on Chrome/Webkit.
|
||||
|
||||
## [0.20.10] - 2021-04-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed zwave/zwavejs2mqtt interoperability.
|
||||
|
||||
## [0.20.9] - 2021-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added zwavejs2mqtt integration (see [#186](https://git.platypush.tech/platypush/platypush/-/issues/186).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Major LINT fixes.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed unmaintained integrations: TorrentCast and Booking.com
|
||||
|
||||
## [0.20.8] - 2021-04-04
|
||||
|
||||
### Added
|
||||
|
||||
- Added `<Camera>` dashboard widget.
|
||||
|
||||
- Added support for custom dashboard widgets with customized (see https://git.platypush.tech/platypush/platypush/-/wikis/Backends#creating-custom-widgets).
|
||||
|
||||
- Added support for controls on `music.mpd` dashboard widget.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed zigbee2mqtt backend error in case of messages with empty payload (see [#184](https://git.platypush.tech/platypush/platypush/-/issues/184)).
|
||||
|
||||
- Fixed compatibility with all versions of websocket-client - versions >= 0.58.0 pass a `WebSocketApp` object as a first
|
||||
argument to the callbacks, as well as versions < 0.54.0 do, but the versions in between don't pass this argument.
|
||||
|
||||
## [0.20.7] - 2021-03-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed race condition on `media.vlc.stop` when clearing the VLC instance.
|
||||
|
||||
- Fixed dashboard widgets custom classes being propagated both to the container and to the widget content [see #179]
|
||||
|
||||
- Fixed compatibility with SQLAlchemy >= 1.4.
|
||||
|
||||
## [0.20.6] - 2021-03-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added `log.http` backend to monitor changes to HTTP log files
|
||||
|
@ -115,12 +17,6 @@ Given the high speed of development in the first phase, changes are being report
|
|||
|
||||
- Removed legacy `pusher` script and `local` backend.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed support for Z-Wave switches.
|
||||
|
||||
- Fixed possible race condition on VLC stop.
|
||||
|
||||
## [0.20.5] - 2021-03-12
|
||||
|
||||
### Added
|
||||
|
|
10
README.md
10
README.md
|
@ -7,8 +7,6 @@ Platypush
|
|||
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
|
||||
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
|
||||
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
|
||||
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
|
||||
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:javascript)
|
||||
|
||||
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
|
||||
|
||||
|
@ -476,15 +474,9 @@ platydock stop device_id
|
|||
4. Remove the instance:
|
||||
|
||||
```shell
|
||||
platydock rm device_id
|
||||
platyvdock rm device_id
|
||||
```
|
||||
|
||||
## Mobile app
|
||||
|
||||
An [official Android app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on the F-Droid store.
|
||||
It allows to easily discover and manage multiple Platypush services on a network through the web interface, and it
|
||||
easily brings the power of Platypush to your fingertips.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import importlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Union, List
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import Directive
|
||||
|
||||
|
||||
class SchemaDirective(Directive):
|
||||
"""
|
||||
Support for response/message schemas in the docs. Format: ``.. schema:: rel_path.SchemaClass(arg1=value1, ...)``,
|
||||
where ``rel_path`` is the path of the schema relative to ``platypush/schemas``.
|
||||
"""
|
||||
has_content = True
|
||||
_schema_regex = re.compile(r'^\s*(.+?)\s*(\((.+?)\))?\s*$')
|
||||
_schemas_path = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.relpath(__file__)), '..', '..', '..', 'platypush', 'schemas'))
|
||||
|
||||
sys.path.insert(0, _schemas_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_field_value(field) -> str:
|
||||
metadata = getattr(field, 'metadata', {})
|
||||
return metadata.get('example', metadata.get('description', str(field.__class__.__name__).lower()))
|
||||
|
||||
def _parse_schema(self) -> Union[dict, List[dict]]:
|
||||
m = self._schema_regex.match('\n'.join(self.content))
|
||||
schema_module_name = '.'.join(['platypush.schemas', *(m.group(1).split('.')[:-1])])
|
||||
schema_module = importlib.import_module(schema_module_name)
|
||||
schema_class = getattr(schema_module, m.group(1).split('.')[-1])
|
||||
schema_args = eval(f'dict({m.group(3)})')
|
||||
schema = schema_class(**schema_args)
|
||||
output = {
|
||||
name: self._get_field_value(field)
|
||||
for name, field in schema.fields.items()
|
||||
if not field.load_only
|
||||
}
|
||||
|
||||
return [output] if schema.many else output
|
||||
|
||||
def run(self):
|
||||
content = json.dumps(self._parse_schema(), sort_keys=True, indent=2)
|
||||
block = nodes.literal_block(content, content)
|
||||
block['language'] = 'json'
|
||||
return [block]
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_directive('schema', SchemaDirective)
|
||||
|
||||
return {
|
||||
'version': '0.1',
|
||||
'parallel_read_safe': True,
|
||||
'parallel_write_safe': True,
|
||||
}
|
|
@ -32,8 +32,6 @@ Backends
|
|||
platypush/backend/http.poll.rst
|
||||
platypush/backend/inotify.rst
|
||||
platypush/backend/joystick.rst
|
||||
platypush/backend/joystick.jstest.rst
|
||||
platypush/backend/joystick.linux.rst
|
||||
platypush/backend/kafka.rst
|
||||
platypush/backend/light.hue.rst
|
||||
platypush/backend/linode.rst
|
||||
|
@ -44,7 +42,6 @@ Backends
|
|||
platypush/backend/music.mopidy.rst
|
||||
platypush/backend/music.mpd.rst
|
||||
platypush/backend/music.snapcast.rst
|
||||
platypush/backend/music.spotify.connect.rst
|
||||
platypush/backend/nextcloud.rst
|
||||
platypush/backend/nfc.rst
|
||||
platypush/backend/nodered.rst
|
||||
|
@ -83,4 +80,3 @@ Backends
|
|||
platypush/backend/wiimote.rst
|
||||
platypush/backend/zigbee.mqtt.rst
|
||||
platypush/backend/zwave.rst
|
||||
platypush/backend/zwave.mqtt.rst
|
||||
|
|
|
@ -18,7 +18,6 @@ import sys
|
|||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.insert(0, os.path.abspath("./_ext"))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
@ -51,7 +50,6 @@ extensions = [
|
|||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.githubpages',
|
||||
'sphinx_rtd_theme',
|
||||
'sphinx_marshmallow',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
|
@ -136,11 +134,6 @@ html_theme_options = {
|
|||
'title': 'Firefox Extension',
|
||||
'internal': False,
|
||||
},
|
||||
{
|
||||
'href': 'https://f-droid.org/en/packages/tech.platypush.platypush/',
|
||||
'title': 'Android App',
|
||||
'internal': False,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ Events
|
|||
platypush/events/gps.rst
|
||||
platypush/events/http.rst
|
||||
platypush/events/http.hook.rst
|
||||
platypush/events/http.ota.booking.rst
|
||||
platypush/events/http.rss.rst
|
||||
platypush/events/inotify.rst
|
||||
platypush/events/joystick.rst
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.backend.joystick.jstest``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.jstest
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.backend.joystick.linux``
|
||||
====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.linux
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.backend.music.spotify.connect``
|
||||
===========================================
|
||||
|
||||
.. automodule:: platypush.backend.music.spotify.connect
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.backend.zwave.mqtt``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.zwave.mqtt
|
||||
:members:
|
5
docs/source/platypush/events/http.ota.booking.rst
Normal file
5
docs/source/platypush/events/http.ota.booking.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.http.ota.booking``
|
||||
============================================
|
||||
|
||||
.. automodule:: platypush.message.event.http.ota.booking
|
||||
:members:
|
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.http.request.ota.booking``
|
||||
==============================================
|
||||
|
||||
.. automodule:: platypush.plugins.http.request.ota.booking
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.pwm.pca9685``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.plugins.pwm.pca9685
|
||||
:members:
|
6
docs/source/platypush/plugins/switch.switchbot.rst
Normal file
6
docs/source/platypush/plugins/switch.switchbot.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
``platypush.plugins.switch.switchbot``
|
||||
======================================
|
||||
|
||||
.. automodule:: platypush.plugins.switch.switchbot
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.switchbot.bluetooth``
|
||||
=========================================
|
||||
|
||||
.. automodule:: platypush.plugins.switchbot.bluetooth
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.switchbot``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.switchbot
|
||||
:members:
|
5
docs/source/platypush/plugins/video.torrentcast.rst
Normal file
5
docs/source/platypush/plugins/video.torrentcast.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.video.torrentcast``
|
||||
=======================================
|
||||
|
||||
.. automodule:: platypush.plugins.video.torrentcast
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.zwave._base``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.plugins.zwave._base
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.zwave.mqtt``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.plugins.zwave.mqtt
|
||||
:members:
|
|
@ -61,6 +61,7 @@ Plugins
|
|||
platypush/plugins/graphite.rst
|
||||
platypush/plugins/homeseer.rst
|
||||
platypush/plugins/http.request.rst
|
||||
platypush/plugins/http.request.ota.booking.rst
|
||||
platypush/plugins/http.request.rss.rst
|
||||
platypush/plugins/http.webpage.rst
|
||||
platypush/plugins/ifttt.rst
|
||||
|
@ -104,7 +105,6 @@ Plugins
|
|||
platypush/plugins/ping.rst
|
||||
platypush/plugins/printer.cups.rst
|
||||
platypush/plugins/pushbullet.rst
|
||||
platypush/plugins/pwm.pca9685.rst
|
||||
platypush/plugins/qrcode.rst
|
||||
platypush/plugins/redis.rst
|
||||
platypush/plugins/rtorrent.rst
|
||||
|
@ -119,10 +119,9 @@ Plugins
|
|||
platypush/plugins/stt.picovoice.hotword.rst
|
||||
platypush/plugins/stt.picovoice.speech.rst
|
||||
platypush/plugins/switch.rst
|
||||
platypush/plugins/switch.switchbot.rst
|
||||
platypush/plugins/switch.tplink.rst
|
||||
platypush/plugins/switch.wemo.rst
|
||||
platypush/plugins/switchbot.rst
|
||||
platypush/plugins/switchbot.bluetooth.rst
|
||||
platypush/plugins/system.rst
|
||||
platypush/plugins/tcp.rst
|
||||
platypush/plugins/tensorflow.rst
|
||||
|
@ -138,6 +137,7 @@ Plugins
|
|||
platypush/plugins/user.rst
|
||||
platypush/plugins/utils.rst
|
||||
platypush/plugins/variable.rst
|
||||
platypush/plugins/video.torrentcast.rst
|
||||
platypush/plugins/weather.rst
|
||||
platypush/plugins/weather.buienradar.rst
|
||||
platypush/plugins/weather.darksky.rst
|
||||
|
@ -147,5 +147,3 @@ Plugins
|
|||
platypush/plugins/zeroconf.rst
|
||||
platypush/plugins/zigbee.mqtt.rst
|
||||
platypush/plugins/zwave.rst
|
||||
platypush/plugins/zwave._base.rst
|
||||
platypush/plugins/zwave.mqtt.rst
|
||||
|
|
|
@ -24,7 +24,7 @@ from .utils import set_thread_name
|
|||
|
||||
|
||||
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
||||
__version__ = '0.21.1'
|
||||
__version__ = '0.20.6'
|
||||
|
||||
logger = logging.getLogger('platypush')
|
||||
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
import logging
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from threading import Thread, Event as ThreadEvent, get_ident
|
||||
from threading import Thread
|
||||
from typing import Optional, Dict
|
||||
|
||||
from platypush.bus import Bus
|
||||
|
@ -61,7 +62,7 @@ class Backend(Thread, EventGenerator):
|
|||
self.poll_seconds = float(poll_seconds) if poll_seconds else None
|
||||
self.device_id = Config.get('device_id')
|
||||
self.thread_id = None
|
||||
self._stop_event = ThreadEvent()
|
||||
self._stop_event = threading.Event()
|
||||
self._kwargs = kwargs
|
||||
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
|
||||
self.zeroconf = None
|
||||
|
@ -219,7 +220,7 @@ class Backend(Thread, EventGenerator):
|
|||
|
||||
def run(self):
|
||||
""" Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """
|
||||
self.thread_id = get_ident()
|
||||
self.thread_id = threading.get_ident()
|
||||
set_thread_name(self._thread_name)
|
||||
if not callable(self.loop):
|
||||
return
|
||||
|
|
|
@ -65,11 +65,11 @@ class AdafruitIoBackend(Backend):
|
|||
def on_message(self, msg):
|
||||
# noinspection PyUnusedLocal
|
||||
def _handler(client, feed, data):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
data = float(data)
|
||||
except Exception as e:
|
||||
self.logger.debug('Not a number: {}: {}'.format(data, e))
|
||||
|
||||
except:
|
||||
pass
|
||||
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
|
||||
|
||||
return _handler
|
||||
|
|
|
@ -55,17 +55,16 @@ class Alarm:
|
|||
self._runtime_snooze_interval = snooze_interval
|
||||
|
||||
def get_next(self) -> float:
|
||||
now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
||||
now = datetime.datetime.now().replace(tzinfo=gettz())
|
||||
|
||||
try:
|
||||
cron = croniter.croniter(self.when, now)
|
||||
return cron.get_next()
|
||||
except (AttributeError, croniter.CroniterBadCronError):
|
||||
try:
|
||||
timestamp = datetime.datetime.fromisoformat(self.when).replace(
|
||||
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
||||
timestamp = datetime.datetime.fromisoformat(self.when).replace(tzinfo=gettz())
|
||||
except (TypeError, ValueError):
|
||||
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable]
|
||||
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) +
|
||||
datetime.timedelta(seconds=int(self.when)))
|
||||
|
||||
return timestamp.timestamp() if timestamp >= now else None
|
||||
|
|
|
@ -13,41 +13,37 @@ Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
|
|||
import asyncio
|
||||
from enum import Enum
|
||||
from collections import namedtuple
|
||||
import time
|
||||
import struct
|
||||
import itertools
|
||||
|
||||
|
||||
class CreateConnectionChannelError(Enum):
|
||||
NoError = 0
|
||||
MaxPendingConnectionsReached = 1
|
||||
|
||||
|
||||
class ConnectionStatus(Enum):
|
||||
Disconnected = 0
|
||||
Connected = 1
|
||||
Ready = 2
|
||||
|
||||
|
||||
class DisconnectReason(Enum):
|
||||
Unspecified = 0
|
||||
ConnectionEstablishmentFailed = 1
|
||||
TimedOut = 2
|
||||
BondingKeysMismatch = 3
|
||||
|
||||
|
||||
class RemovedReason(Enum):
|
||||
RemovedByThisClient = 0
|
||||
ForceDisconnectedByThisClient = 1
|
||||
ForceDisconnectedByOtherClient = 2
|
||||
|
||||
|
||||
ButtonIsPrivate = 3
|
||||
VerifyTimeout = 4
|
||||
InternetBackendError = 5
|
||||
InvalidData = 6
|
||||
|
||||
|
||||
CouldntLoadDevice = 7
|
||||
|
||||
|
||||
class ClickType(Enum):
|
||||
ButtonDown = 0
|
||||
ButtonUp = 1
|
||||
|
@ -56,24 +52,20 @@ class ClickType(Enum):
|
|||
ButtonDoubleClick = 4
|
||||
ButtonHold = 5
|
||||
|
||||
|
||||
class BdAddrType(Enum):
|
||||
PublicBdAddrType = 0
|
||||
RandomBdAddrType = 1
|
||||
|
||||
|
||||
class LatencyMode(Enum):
|
||||
NormalLatency = 0
|
||||
LowLatency = 1
|
||||
HighLatency = 2
|
||||
|
||||
|
||||
class BluetoothControllerState(Enum):
|
||||
Detached = 0
|
||||
Resetting = 1
|
||||
Attached = 2
|
||||
|
||||
|
||||
class ScanWizardResult(Enum):
|
||||
WizardSuccess = 0
|
||||
WizardCancelledByUser = 1
|
||||
|
@ -83,26 +75,24 @@ class ScanWizardResult(Enum):
|
|||
WizardInternetBackendError = 5
|
||||
WizardInvalidData = 6
|
||||
|
||||
|
||||
class ButtonScanner:
|
||||
"""ButtonScanner class.
|
||||
|
||||
|
||||
Usage:
|
||||
scanner = ButtonScanner()
|
||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
||||
client.add_scanner(scanner)
|
||||
"""
|
||||
|
||||
|
||||
_cnt = itertools.count()
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._scan_id = next(ButtonScanner._cnt)
|
||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
||||
|
||||
|
||||
class ScanWizard:
|
||||
"""ScanWizard class
|
||||
|
||||
|
||||
Usage:
|
||||
wizard = ScanWizard()
|
||||
wizard.on_found_private_button = lambda scan_wizard: ...
|
||||
|
@ -111,9 +101,9 @@ class ScanWizard:
|
|||
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
||||
client.add_scan_wizard(wizard)
|
||||
"""
|
||||
|
||||
|
||||
_cnt = itertools.count()
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._scan_wizard_id = next(ScanWizard._cnt)
|
||||
self._bd_addr = None
|
||||
|
@ -123,34 +113,33 @@ class ScanWizard:
|
|||
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
|
||||
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
|
||||
|
||||
|
||||
class ButtonConnectionChannel:
|
||||
"""ButtonConnectionChannel class.
|
||||
|
||||
|
||||
This class represents a connection channel to a Flic button.
|
||||
Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel).
|
||||
You may only have this connection channel added to one FlicClient at a time.
|
||||
|
||||
|
||||
Before you add the connection channel to the client, you should set up your callback functions by assigning
|
||||
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
|
||||
referencing this object.
|
||||
|
||||
|
||||
Available properties and the function parameters are:
|
||||
on_create_connection_channel_response: channel, error, connection_status
|
||||
on_removed: channel, removed_reason
|
||||
on_connection_status_changed: channel, connection_status, disconnect_reason
|
||||
on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff
|
||||
"""
|
||||
|
||||
|
||||
_cnt = itertools.count()
|
||||
|
||||
def __init__(self, bd_addr, latency_mode=LatencyMode.NormalLatency, auto_disconnect_time=511):
|
||||
|
||||
def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511):
|
||||
self._conn_id = next(ButtonConnectionChannel._cnt)
|
||||
self._bd_addr = bd_addr
|
||||
self._latency_mode = latency_mode
|
||||
self._auto_disconnect_time = auto_disconnect_time
|
||||
self._client = None
|
||||
|
||||
|
||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
||||
self.on_removed = lambda channel, removed_reason: None
|
||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None
|
||||
|
@ -158,66 +147,61 @@ class ButtonConnectionChannel:
|
|||
self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||
self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None
|
||||
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||
|
||||
|
||||
@property
|
||||
def bd_addr(self):
|
||||
return self._bd_addr
|
||||
|
||||
|
||||
@property
|
||||
def latency_mode(self):
|
||||
return self._latency_mode
|
||||
|
||||
|
||||
@latency_mode.setter
|
||||
def latency_mode(self, latency_mode):
|
||||
if self._client is None:
|
||||
self._latency_mode = latency_mode
|
||||
return
|
||||
|
||||
|
||||
self._latency_mode = latency_mode
|
||||
if not self._client._closed:
|
||||
self._client._send_command("CmdChangeModeParameters",
|
||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
||||
"auto_disconnect_time": self._auto_disconnect_time})
|
||||
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||
|
||||
@property
|
||||
def auto_disconnect_time(self):
|
||||
return self._auto_disconnect_time
|
||||
|
||||
|
||||
@auto_disconnect_time.setter
|
||||
def auto_disconnect_time(self, auto_disconnect_time):
|
||||
if self._client is None:
|
||||
self._auto_disconnect_time = auto_disconnect_time
|
||||
return
|
||||
|
||||
|
||||
self._auto_disconnect_time = auto_disconnect_time
|
||||
if not self._client._closed:
|
||||
self._client._send_command("CmdChangeModeParameters",
|
||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
||||
"auto_disconnect_time": self._auto_disconnect_time})
|
||||
|
||||
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||
|
||||
class FlicClient(asyncio.Protocol):
|
||||
"""FlicClient class.
|
||||
|
||||
|
||||
When this class is constructed, a socket connection is established.
|
||||
You may then send commands to the server and set timers.
|
||||
Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed.
|
||||
For a more detailed description of all commands, events and enums, check the protocol specification.
|
||||
|
||||
|
||||
All commands are wrapped in more high level functions and events are reported using callback functions.
|
||||
|
||||
|
||||
All methods called on this class will take effect only if you eventually call the handle_events() method.
|
||||
|
||||
|
||||
The ButtonScanner is used to set up a handler for advertisement packets.
|
||||
The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events.
|
||||
|
||||
|
||||
Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters):
|
||||
on_new_verified_button: bd_addr
|
||||
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
||||
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
||||
on_bluetooth_controller_state_change: state
|
||||
"""
|
||||
|
||||
|
||||
_EVENTS = [
|
||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
||||
|
@ -228,8 +212,7 @@ class FlicClient(asyncio.Protocol):
|
|||
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
||||
("EvtGetInfoResponse", "<B6sBBhBBH",
|
||||
"bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
|
||||
("EvtGetInfoResponse", "<B6sBBhBBH", "bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
|
||||
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||
("EvtBluetoothControllerStateChange", "<B", "state"),
|
||||
|
@ -240,9 +223,9 @@ class FlicClient(asyncio.Protocol):
|
|||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
||||
]
|
||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
|
||||
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
|
||||
|
||||
_EVENT_STRUCTS = list(map(lambda x: None if x == None else struct.Struct(x[1]), _EVENTS))
|
||||
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
|
||||
|
||||
_COMMANDS = [
|
||||
("CmdGetInfo", "", ""),
|
||||
("CmdCreateScanner", "<I", "scan_id"),
|
||||
|
@ -256,169 +239,168 @@ class FlicClient(asyncio.Protocol):
|
|||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
||||
]
|
||||
|
||||
|
||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
||||
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
|
||||
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||
|
||||
def __init__(self, loop, parent=None):
|
||||
|
||||
def __init__(self, loop,parent=None):
|
||||
self.loop = loop
|
||||
self.buffer = b""
|
||||
self.transport = None
|
||||
self.parent = parent
|
||||
self.buffer=b""
|
||||
self.transport=None
|
||||
self.parent=parent
|
||||
self._scanners = {}
|
||||
self._scan_wizards = {}
|
||||
self._connection_channels = {}
|
||||
self._closed = False
|
||||
|
||||
|
||||
self.on_new_verified_button = lambda bd_addr: None
|
||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
||||
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
||||
self.on_bluetooth_controller_state_change = lambda state: None
|
||||
self.on_bluetooth_controller_state_change = lambda state: None
|
||||
self.on_get_info = lambda items: None
|
||||
self.on_get_button_uuid = lambda addr, uuid: None
|
||||
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
self.transport=transport
|
||||
if self.parent:
|
||||
self.parent.register_protocol(self)
|
||||
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Closes the client. The handle_events() method will return."""
|
||||
if self._closed:
|
||||
return
|
||||
|
||||
self._closed = True
|
||||
|
||||
|
||||
def add_scanner(self, scanner):
|
||||
"""Add a ButtonScanner object.
|
||||
|
||||
|
||||
The scan will start directly once the scanner is added.
|
||||
"""
|
||||
if scanner._scan_id in self._scanners:
|
||||
return
|
||||
|
||||
|
||||
self._scanners[scanner._scan_id] = scanner
|
||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
||||
|
||||
|
||||
def remove_scanner(self, scanner):
|
||||
"""Remove a ButtonScanner object.
|
||||
|
||||
|
||||
You will no longer receive advertisement packets.
|
||||
"""
|
||||
if scanner._scan_id not in self._scanners:
|
||||
return
|
||||
|
||||
|
||||
del self._scanners[scanner._scan_id]
|
||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
||||
|
||||
|
||||
def add_scan_wizard(self, scan_wizard):
|
||||
"""Add a ScanWizard object.
|
||||
|
||||
|
||||
The scan wizard will start directly once the scan wizard is added.
|
||||
"""
|
||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||
return
|
||||
|
||||
|
||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||
|
||||
|
||||
def cancel_scan_wizard(self, scan_wizard):
|
||||
"""Cancel a ScanWizard.
|
||||
|
||||
|
||||
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
|
||||
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
||||
"""
|
||||
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||
return
|
||||
|
||||
|
||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||
|
||||
|
||||
def add_connection_channel(self, channel):
|
||||
"""Adds a connection channel to a specific Flic button.
|
||||
|
||||
|
||||
This will start listening for a specific Flic button's connection and button events.
|
||||
Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
|
||||
|
||||
|
||||
The on_create_connection_channel_response callback property will be called on the
|
||||
connection channel after this command has been received by the server.
|
||||
|
||||
|
||||
You may have as many connection channels as you wish for a specific Flic Button.
|
||||
"""
|
||||
if channel._conn_id in self._connection_channels:
|
||||
return
|
||||
|
||||
|
||||
channel._client = self
|
||||
|
||||
|
||||
self._connection_channels[channel._conn_id] = channel
|
||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr,
|
||||
"latency_mode": channel._latency_mode,
|
||||
"auto_disconnect_time": channel._auto_disconnect_time})
|
||||
|
||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
|
||||
|
||||
def remove_connection_channel(self, channel):
|
||||
"""Remove a connection channel.
|
||||
|
||||
|
||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
||||
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
|
||||
"""
|
||||
if channel._conn_id not in self._connection_channels:
|
||||
return
|
||||
|
||||
|
||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
||||
|
||||
|
||||
def force_disconnect(self, bd_addr):
|
||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
||||
|
||||
|
||||
This removes all connection channels for all clients connected to the server for this specific Flic button.
|
||||
"""
|
||||
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||
|
||||
|
||||
def get_info(self):
|
||||
"""Get info about the current state of the server.
|
||||
|
||||
|
||||
The server will send back its information directly and the callback will be called once the response arrives.
|
||||
The callback takes only one parameter: info. This info parameter is a dictionary with the following objects:
|
||||
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
|
||||
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
|
||||
"""
|
||||
self._send_command("CmdGetInfo", {})
|
||||
|
||||
|
||||
def get_button_uuid(self, bd_addr):
|
||||
"""Get button uuid for a verified button.
|
||||
|
||||
|
||||
The server will send back its information directly and the callback will be called once the response arrives.
|
||||
Responses will arrive in the same order as requested.
|
||||
|
||||
|
||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
||||
|
||||
|
||||
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
||||
"""
|
||||
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
||||
|
||||
|
||||
|
||||
def run_on_handle_events_thread(self, callback):
|
||||
"""Run a function on the thread that handles the events."""
|
||||
if threading.get_ident() == self._handle_event_thread_ident:
|
||||
callback()
|
||||
else:
|
||||
self.set_timer(0, callback)
|
||||
|
||||
|
||||
def _send_command(self, name, items):
|
||||
|
||||
|
||||
for key, value in items.items():
|
||||
if isinstance(value, Enum):
|
||||
items[key] = value.value
|
||||
|
||||
|
||||
if "bd_addr" in items:
|
||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes()
|
||||
|
||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
||||
|
||||
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
||||
bytes = bytearray(3)
|
||||
|
@ -427,85 +409,83 @@ class FlicClient(asyncio.Protocol):
|
|||
bytes[2] = opcode
|
||||
bytes += data_bytes
|
||||
self.transport.write(bytes)
|
||||
|
||||
|
||||
def _dispatch_event(self, data):
|
||||
if len(data) == 0:
|
||||
return
|
||||
opcode = data[0]
|
||||
|
||||
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None:
|
||||
|
||||
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
|
||||
return
|
||||
|
||||
|
||||
event_name = FlicClient._EVENTS[opcode][0]
|
||||
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1: 1 + FlicClient._EVENT_STRUCTS[opcode].size])
|
||||
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size])
|
||||
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||
|
||||
|
||||
# Process some kind of items whose data type is not supported by struct
|
||||
if "bd_addr" in items:
|
||||
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string()
|
||||
|
||||
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
||||
|
||||
if "name" in items:
|
||||
items["name"] = items["name"].decode("utf-8")
|
||||
|
||||
|
||||
if event_name == "EvtCreateConnectionChannelResponse":
|
||||
items["error"] = CreateConnectionChannelError(items["error"])
|
||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionStatusChanged":
|
||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionChannelRemoved":
|
||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||
|
||||
|
||||
if event_name.startswith("EvtButton"):
|
||||
items["click_type"] = ClickType(items["click_type"])
|
||||
|
||||
|
||||
if event_name == "EvtGetInfoResponse":
|
||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
||||
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string()
|
||||
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
|
||||
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||
items["bd_addr_of_verified_buttons"] = []
|
||||
|
||||
|
||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||
for i in range(items["nb_verified_buttons"]):
|
||||
items["bd_addr_of_verified_buttons"].append(
|
||||
FlicClient._bdaddr_bytes_to_string())
|
||||
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
||||
pos += 6
|
||||
|
||||
|
||||
if event_name == "EvtBluetoothControllerStateChange":
|
||||
items["state"] = BluetoothControllerState(items["state"])
|
||||
|
||||
|
||||
if event_name == "EvtGetButtonUUIDResponse":
|
||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||
if items["uuid"] == "00000000000000000000000000000000":
|
||||
items["uuid"] = None
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardCompleted":
|
||||
items["result"] = ScanWizardResult(items["result"])
|
||||
|
||||
|
||||
# Process event
|
||||
if event_name == "EvtAdvertisementPacket":
|
||||
scanner = self._scanners.get(items["scan_id"])
|
||||
if scanner is not None:
|
||||
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"],
|
||||
items["is_private"], items["already_verified"])
|
||||
|
||||
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
|
||||
|
||||
if event_name == "EvtCreateConnectionChannelResponse":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
if items["error"] != CreateConnectionChannelError.NoError:
|
||||
del self._connection_channels[items["conn_id"]]
|
||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionStatusChanged":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionChannelRemoved":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
del self._connection_channels[items["conn_id"]]
|
||||
channel.on_removed(channel, items["removed_reason"])
|
||||
|
||||
|
||||
if event_name == "EvtButtonUpOrDown":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||
|
@ -514,60 +494,61 @@ class FlicClient(asyncio.Protocol):
|
|||
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||
if event_name == "EvtButtonSingleOrDoubleClick":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"],
|
||||
items["time_diff"])
|
||||
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"],
|
||||
items["time_diff"])
|
||||
|
||||
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||
|
||||
if event_name == "EvtNewVerifiedButton":
|
||||
self.on_new_verified_button(items["bd_addr"])
|
||||
|
||||
|
||||
if event_name == "EvtGetInfoResponse":
|
||||
self.on_get_info(items)
|
||||
|
||||
|
||||
if event_name == "EvtNoSpaceForNewConnection":
|
||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||
|
||||
|
||||
if event_name == "EvtGotSpaceForNewConnection":
|
||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||
|
||||
|
||||
if event_name == "EvtBluetoothControllerStateChange":
|
||||
self.on_bluetooth_controller_state_change(items["state"])
|
||||
|
||||
|
||||
if event_name == "EvtGetButtonUUIDResponse":
|
||||
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard.on_found_private_button(scan_wizard)
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardFoundPublicButton":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard._bd_addr = items["bd_addr"]
|
||||
scan_wizard._name = items["name"]
|
||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardButtonConnected":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardCompleted":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
del self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
|
||||
|
||||
def data_received(self, data):
|
||||
cdata = self.buffer + data
|
||||
self.buffer = b""
|
||||
|
||||
|
||||
def data_received(self,data):
|
||||
cdata=self.buffer+data
|
||||
self.buffer=b""
|
||||
while len(cdata):
|
||||
packet_len = cdata[0] | (cdata[1] << 8)
|
||||
packet_len += 2
|
||||
if len(cdata) >= packet_len:
|
||||
if len(cdata)>= packet_len:
|
||||
self._dispatch_event(cdata[2:packet_len])
|
||||
cdata = cdata[packet_len:]
|
||||
cdata=cdata[packet_len:]
|
||||
else:
|
||||
if len(cdata):
|
||||
self.buffer = cdata # unlikely to happen but.....
|
||||
self.buffer=cdata #unlikely to happen but.....
|
||||
break
|
||||
|
||||
|
||||
|
|
|
@ -40,12 +40,12 @@ class RemovedReason(Enum):
|
|||
RemovedByThisClient = 0
|
||||
ForceDisconnectedByThisClient = 1
|
||||
ForceDisconnectedByOtherClient = 2
|
||||
|
||||
|
||||
ButtonIsPrivate = 3
|
||||
VerifyTimeout = 4
|
||||
InternetBackendError = 5
|
||||
InvalidData = 6
|
||||
|
||||
|
||||
CouldntLoadDevice = 7
|
||||
|
||||
class ClickType(Enum):
|
||||
|
@ -81,22 +81,22 @@ class ScanWizardResult(Enum):
|
|||
|
||||
class ButtonScanner:
|
||||
"""ButtonScanner class.
|
||||
|
||||
|
||||
Usage:
|
||||
scanner = ButtonScanner()
|
||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
||||
client.add_scanner(scanner)
|
||||
"""
|
||||
|
||||
|
||||
_cnt = itertools.count()
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._scan_id = next(ButtonScanner._cnt)
|
||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
||||
|
||||
class ScanWizard:
|
||||
"""ScanWizard class
|
||||
|
||||
|
||||
Usage:
|
||||
wizard = ScanWizard()
|
||||
wizard.on_found_private_button = lambda scan_wizard: ...
|
||||
|
@ -105,9 +105,9 @@ class ScanWizard:
|
|||
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
||||
client.add_scan_wizard(wizard)
|
||||
"""
|
||||
|
||||
|
||||
_cnt = itertools.count()
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._scan_wizard_id = next(ScanWizard._cnt)
|
||||
self._bd_addr = None
|
||||
|
@ -119,31 +119,31 @@ class ScanWizard:
|
|||
|
||||
class ButtonConnectionChannel:
|
||||
"""ButtonConnectionChannel class.
|
||||
|
||||
|
||||
This class represents a connection channel to a Flic button.
|
||||
Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel).
|
||||
You may only have this connection channel added to one FlicClient at a time.
|
||||
|
||||
|
||||
Before you add the connection channel to the client, you should set up your callback functions by assigning
|
||||
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
|
||||
referencing this object.
|
||||
|
||||
|
||||
Available properties and the function parameters are:
|
||||
on_create_connection_channel_response: channel, error, connection_status
|
||||
on_removed: channel, removed_reason
|
||||
on_connection_status_changed: channel, connection_status, disconnect_reason
|
||||
on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff
|
||||
"""
|
||||
|
||||
|
||||
_cnt = itertools.count()
|
||||
|
||||
|
||||
def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511):
|
||||
self._conn_id = next(ButtonConnectionChannel._cnt)
|
||||
self._bd_addr = bd_addr
|
||||
self._latency_mode = latency_mode
|
||||
self._auto_disconnect_time = auto_disconnect_time
|
||||
self._client = None
|
||||
|
||||
|
||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
||||
self.on_removed = lambda channel, removed_reason: None
|
||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None
|
||||
|
@ -151,36 +151,36 @@ class ButtonConnectionChannel:
|
|||
self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||
self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None
|
||||
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||
|
||||
|
||||
@property
|
||||
def bd_addr(self):
|
||||
return self._bd_addr
|
||||
|
||||
|
||||
@property
|
||||
def latency_mode(self):
|
||||
return self._latency_mode
|
||||
|
||||
|
||||
@latency_mode.setter
|
||||
def latency_mode(self, latency_mode):
|
||||
if self._client is None:
|
||||
self._latency_mode = latency_mode
|
||||
return
|
||||
|
||||
|
||||
with self._client._lock:
|
||||
self._latency_mode = latency_mode
|
||||
if not self._client._closed:
|
||||
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||
|
||||
|
||||
@property
|
||||
def auto_disconnect_time(self):
|
||||
return self._auto_disconnect_time
|
||||
|
||||
|
||||
@auto_disconnect_time.setter
|
||||
def auto_disconnect_time(self, auto_disconnect_time):
|
||||
if self._client is None:
|
||||
self._auto_disconnect_time = auto_disconnect_time
|
||||
return
|
||||
|
||||
|
||||
with self._client._lock:
|
||||
self._auto_disconnect_time = auto_disconnect_time
|
||||
if not self._client._closed:
|
||||
|
@ -188,26 +188,26 @@ class ButtonConnectionChannel:
|
|||
|
||||
class FlicClient:
|
||||
"""FlicClient class.
|
||||
|
||||
|
||||
When this class is constructed, a socket connection is established.
|
||||
You may then send commands to the server and set timers.
|
||||
Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed.
|
||||
For a more detailed description of all commands, events and enums, check the protocol specification.
|
||||
|
||||
|
||||
All commands are wrapped in more high level functions and events are reported using callback functions.
|
||||
|
||||
|
||||
All methods called on this class will take effect only if you eventually call the handle_events() method.
|
||||
|
||||
|
||||
The ButtonScanner is used to set up a handler for advertisement packets.
|
||||
The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events.
|
||||
|
||||
|
||||
Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters):
|
||||
on_new_verified_button: bd_addr
|
||||
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
||||
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
||||
on_bluetooth_controller_state_change: state
|
||||
"""
|
||||
|
||||
|
||||
_EVENTS = [
|
||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
||||
|
@ -229,9 +229,9 @@ class FlicClient:
|
|||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
||||
]
|
||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
|
||||
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
|
||||
|
||||
_EVENT_STRUCTS = list(map(lambda x: None if x == None else struct.Struct(x[1]), _EVENTS))
|
||||
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
|
||||
|
||||
_COMMANDS = [
|
||||
("CmdGetInfo", "", ""),
|
||||
("CmdCreateScanner", "<I", "scan_id"),
|
||||
|
@ -245,19 +245,17 @@ class FlicClient:
|
|||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
||||
]
|
||||
|
||||
|
||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
||||
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
|
||||
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||
|
||||
|
||||
def __init__(self, host, port = 5551):
|
||||
self._sock = socket.create_connection((host, port), None)
|
||||
self._lock = threading.RLock()
|
||||
|
@ -269,113 +267,113 @@ class FlicClient:
|
|||
self._timers = queue.PriorityQueue()
|
||||
self._handle_event_thread_ident = None
|
||||
self._closed = False
|
||||
|
||||
|
||||
self.on_new_verified_button = lambda bd_addr: None
|
||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
||||
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
|
||||
self.on_bluetooth_controller_state_change = lambda state: None
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Closes the client. The handle_events() method will return."""
|
||||
with self._lock:
|
||||
if self._closed:
|
||||
return
|
||||
|
||||
|
||||
if threading.get_ident() != self._handle_event_thread_ident:
|
||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||
|
||||
|
||||
self._closed = True
|
||||
|
||||
|
||||
def add_scanner(self, scanner):
|
||||
"""Add a ButtonScanner object.
|
||||
|
||||
|
||||
The scan will start directly once the scanner is added.
|
||||
"""
|
||||
with self._lock:
|
||||
if scanner._scan_id in self._scanners:
|
||||
return
|
||||
|
||||
|
||||
self._scanners[scanner._scan_id] = scanner
|
||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
||||
|
||||
|
||||
def remove_scanner(self, scanner):
|
||||
"""Remove a ButtonScanner object.
|
||||
|
||||
|
||||
You will no longer receive advertisement packets.
|
||||
"""
|
||||
with self._lock:
|
||||
if scanner._scan_id not in self._scanners:
|
||||
return
|
||||
|
||||
|
||||
del self._scanners[scanner._scan_id]
|
||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
||||
|
||||
|
||||
def add_scan_wizard(self, scan_wizard):
|
||||
"""Add a ScanWizard object.
|
||||
|
||||
|
||||
The scan wizard will start directly once the scan wizard is added.
|
||||
"""
|
||||
with self._lock:
|
||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||
return
|
||||
|
||||
|
||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||
|
||||
|
||||
def cancel_scan_wizard(self, scan_wizard):
|
||||
"""Cancel a ScanWizard.
|
||||
|
||||
|
||||
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
|
||||
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
||||
"""
|
||||
with self._lock:
|
||||
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||
return
|
||||
|
||||
|
||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||
|
||||
|
||||
def add_connection_channel(self, channel):
|
||||
"""Adds a connection channel to a specific Flic button.
|
||||
|
||||
|
||||
This will start listening for a specific Flic button's connection and button events.
|
||||
Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
|
||||
|
||||
|
||||
The on_create_connection_channel_response callback property will be called on the
|
||||
connection channel after this command has been received by the server.
|
||||
|
||||
|
||||
You may have as many connection channels as you wish for a specific Flic Button.
|
||||
"""
|
||||
with self._lock:
|
||||
if channel._conn_id in self._connection_channels:
|
||||
return
|
||||
|
||||
|
||||
channel._client = self
|
||||
|
||||
|
||||
self._connection_channels[channel._conn_id] = channel
|
||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
|
||||
|
||||
|
||||
def remove_connection_channel(self, channel):
|
||||
"""Remove a connection channel.
|
||||
|
||||
|
||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
||||
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
|
||||
"""
|
||||
with self._lock:
|
||||
if channel._conn_id not in self._connection_channels:
|
||||
return
|
||||
|
||||
|
||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
||||
|
||||
|
||||
def force_disconnect(self, bd_addr):
|
||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
||||
|
||||
|
||||
This removes all connection channels for all clients connected to the server for this specific Flic button.
|
||||
"""
|
||||
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||
|
||||
|
||||
def get_info(self, callback):
|
||||
"""Get info about the current state of the server.
|
||||
|
||||
|
||||
The server will send back its information directly and the callback will be called once the response arrives.
|
||||
The callback takes only one parameter: info. This info parameter is a dictionary with the following objects:
|
||||
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
|
||||
|
@ -383,47 +381,47 @@ class FlicClient:
|
|||
"""
|
||||
self._get_info_response_queue.put(callback)
|
||||
self._send_command("CmdGetInfo", {})
|
||||
|
||||
|
||||
def get_button_uuid(self, bd_addr, callback):
|
||||
"""Get button uuid for a verified button.
|
||||
|
||||
|
||||
The server will send back its information directly and the callback will be called once the response arrives.
|
||||
Responses will arrive in the same order as requested.
|
||||
|
||||
|
||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
||||
|
||||
|
||||
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
||||
"""
|
||||
with self._lock:
|
||||
self._get_button_uuid_queue.put(callback)
|
||||
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
||||
|
||||
|
||||
def set_timer(self, timeout_millis, callback):
|
||||
"""Set a timer
|
||||
|
||||
|
||||
This timer callback will run after the specified timeout_millis on the thread that handles the events.
|
||||
"""
|
||||
point_in_time = time.monotonic() + timeout_millis / 1000.0
|
||||
self._timers.put((point_in_time, callback))
|
||||
|
||||
|
||||
if threading.get_ident() != self._handle_event_thread_ident:
|
||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||
|
||||
|
||||
def run_on_handle_events_thread(self, callback):
|
||||
"""Run a function on the thread that handles the events."""
|
||||
if threading.get_ident() == self._handle_event_thread_ident:
|
||||
callback()
|
||||
else:
|
||||
self.set_timer(0, callback)
|
||||
|
||||
|
||||
def _send_command(self, name, items):
|
||||
for key, value in items.items():
|
||||
if isinstance(value, Enum):
|
||||
items[key] = value.value
|
||||
|
||||
|
||||
if "bd_addr" in items:
|
||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
||||
|
||||
|
||||
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
||||
bytes = bytearray(3)
|
||||
|
@ -434,83 +432,83 @@ class FlicClient:
|
|||
with self._lock:
|
||||
if not self._closed:
|
||||
self._sock.sendall(bytes)
|
||||
|
||||
|
||||
def _dispatch_event(self, data):
|
||||
if len(data) == 0:
|
||||
return
|
||||
opcode = data[0]
|
||||
|
||||
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None:
|
||||
|
||||
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
|
||||
return
|
||||
|
||||
|
||||
event_name = FlicClient._EVENTS[opcode][0]
|
||||
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size])
|
||||
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||
|
||||
|
||||
# Process some kind of items whose data type is not supported by struct
|
||||
if "bd_addr" in items:
|
||||
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
||||
|
||||
|
||||
if "name" in items:
|
||||
items["name"] = items["name"].decode("utf-8")
|
||||
|
||||
|
||||
if event_name == "EvtCreateConnectionChannelResponse":
|
||||
items["error"] = CreateConnectionChannelError(items["error"])
|
||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionStatusChanged":
|
||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionChannelRemoved":
|
||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||
|
||||
|
||||
if event_name.startswith("EvtButton"):
|
||||
items["click_type"] = ClickType(items["click_type"])
|
||||
|
||||
|
||||
if event_name == "EvtGetInfoResponse":
|
||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
||||
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
|
||||
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||
items["bd_addr_of_verified_buttons"] = []
|
||||
|
||||
|
||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||
for i in range(items["nb_verified_buttons"]):
|
||||
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
||||
pos += 6
|
||||
|
||||
|
||||
if event_name == "EvtBluetoothControllerStateChange":
|
||||
items["state"] = BluetoothControllerState(items["state"])
|
||||
|
||||
|
||||
if event_name == "EvtGetButtonUUIDResponse":
|
||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||
if items["uuid"] == "00000000000000000000000000000000":
|
||||
items["uuid"] = None
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardCompleted":
|
||||
items["result"] = ScanWizardResult(items["result"])
|
||||
|
||||
|
||||
# Process event
|
||||
if event_name == "EvtAdvertisementPacket":
|
||||
scanner = self._scanners.get(items["scan_id"])
|
||||
if scanner is not None:
|
||||
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
|
||||
|
||||
|
||||
if event_name == "EvtCreateConnectionChannelResponse":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
if items["error"] != CreateConnectionChannelError.NoError:
|
||||
del self._connection_channels[items["conn_id"]]
|
||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionStatusChanged":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
||||
|
||||
|
||||
if event_name == "EvtConnectionChannelRemoved":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
del self._connection_channels[items["conn_id"]]
|
||||
channel.on_removed(channel, items["removed_reason"])
|
||||
|
||||
|
||||
if event_name == "EvtButtonUpOrDown":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||
|
@ -523,44 +521,44 @@ class FlicClient:
|
|||
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||
channel = self._connection_channels[items["conn_id"]]
|
||||
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||
|
||||
|
||||
if event_name == "EvtNewVerifiedButton":
|
||||
self.on_new_verified_button(items["bd_addr"])
|
||||
|
||||
|
||||
if event_name == "EvtGetInfoResponse":
|
||||
self._get_info_response_queue.get()(items)
|
||||
|
||||
|
||||
if event_name == "EvtNoSpaceForNewConnection":
|
||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||
|
||||
|
||||
if event_name == "EvtGotSpaceForNewConnection":
|
||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||
|
||||
|
||||
if event_name == "EvtBluetoothControllerStateChange":
|
||||
self.on_bluetooth_controller_state_change(items["state"])
|
||||
|
||||
|
||||
if event_name == "EvtGetButtonUUIDResponse":
|
||||
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard.on_found_private_button(scan_wizard)
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardFoundPublicButton":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard._bd_addr = items["bd_addr"]
|
||||
scan_wizard._name = items["name"]
|
||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardButtonConnected":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||
|
||||
|
||||
if event_name == "EvtScanWizardCompleted":
|
||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||
del self._scan_wizards[items["scan_wizard_id"]]
|
||||
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
|
||||
|
||||
|
||||
def _handle_one_event(self):
|
||||
if len(self._timers.queue) > 0:
|
||||
current_timer = self._timers.queue[0]
|
||||
|
@ -570,10 +568,10 @@ class FlicClient:
|
|||
return True
|
||||
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
|
||||
return True
|
||||
|
||||
|
||||
len_arr = bytearray(2)
|
||||
view = memoryview(len_arr)
|
||||
|
||||
|
||||
toread = 2
|
||||
while toread > 0:
|
||||
nbytes = self._sock.recv_into(view, toread)
|
||||
|
@ -581,7 +579,7 @@ class FlicClient:
|
|||
return False
|
||||
view = view[nbytes:]
|
||||
toread -= nbytes
|
||||
|
||||
|
||||
packet_len = len_arr[0] | (len_arr[1] << 8)
|
||||
data = bytearray(packet_len)
|
||||
view = memoryview(data)
|
||||
|
@ -592,13 +590,13 @@ class FlicClient:
|
|||
return False
|
||||
view = view[nbytes:]
|
||||
toread -= nbytes
|
||||
|
||||
|
||||
self._dispatch_event(data)
|
||||
return True
|
||||
|
||||
|
||||
def handle_events(self):
|
||||
"""Start the main loop for this client.
|
||||
|
||||
|
||||
This method will not return until the socket has been closed.
|
||||
Once it has returned, any use of this FlicClient is illegal.
|
||||
"""
|
||||
|
|
|
@ -34,7 +34,7 @@ class CameraPiBackend(Backend):
|
|||
return self.value == other
|
||||
|
||||
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
||||
def __init__(self, listen_port, bind_address='0.0.0.0', x_resolution=640, y_resolution=480,
|
||||
def __init__(self, listen_port, x_resolution=640, y_resolution=480,
|
||||
redis_queue='platypush/camera/pi',
|
||||
start_recording_on_startup=True,
|
||||
framerate=24, hflip=False, vflip=False,
|
||||
|
@ -49,17 +49,13 @@ class CameraPiBackend(Backend):
|
|||
|
||||
:param listen_port: Port where the camera process will provide the video output while recording
|
||||
:type listen_port: int
|
||||
|
||||
:param bind_address: Bind address (default: 0.0.0.0).
|
||||
:type bind_address: str
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.bind_address = bind_address
|
||||
self.listen_port = listen_port
|
||||
self.server_socket = socket.socket()
|
||||
self.server_socket.bind((self.bind_address, self.listen_port))
|
||||
self.server_socket.bind(('0.0.0.0', self.listen_port))
|
||||
self.server_socket.listen(0)
|
||||
|
||||
import picamera
|
||||
|
@ -138,13 +134,13 @@ class CameraPiBackend(Backend):
|
|||
self.logger.info('Client closed connection')
|
||||
try:
|
||||
self.stop_recording()
|
||||
except Exception as e:
|
||||
self.logger.warning('Could not stop recording: {}'.format(str(e)))
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
self.logger.warning('Could not close connection: {}'.format(str(e)))
|
||||
except:
|
||||
pass
|
||||
|
||||
self.send_camera_action(self.CameraAction.START_RECORDING)
|
||||
|
||||
|
|
|
@ -25,9 +25,6 @@ class EventHandler(FileSystemEventHandler):
|
|||
def on_modified(self, event):
|
||||
get_bus().post(FileSystemModifyEvent(path=event.src_path, is_directory=event.is_directory))
|
||||
|
||||
def on_moved(self, event):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_resource(cls, resource: MonitoredResource):
|
||||
if isinstance(resource, MonitoredPattern):
|
||||
|
|
|
@ -51,10 +51,11 @@ class GooglePubsubBackend(Backend):
|
|||
def _message_callback(self, topic):
|
||||
def callback(msg):
|
||||
data = msg.data.decode()
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except Exception as e:
|
||||
self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e)))
|
||||
except:
|
||||
pass
|
||||
|
||||
msg.ack()
|
||||
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
|
||||
|
|
|
@ -31,8 +31,8 @@ def _hook(hook_name):
|
|||
# noinspection PyBroadException
|
||||
try:
|
||||
event_args['data'] = json.loads(event_args['data'])
|
||||
except Exception as e:
|
||||
logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e)))
|
||||
except:
|
||||
pass
|
||||
|
||||
event = WebhookEvent(**event_args)
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ def login():
|
|||
if session_token:
|
||||
user, session = user_manager.authenticate_user_session(session_token)
|
||||
if user:
|
||||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
return redirect(redirect_page, 302)
|
||||
|
||||
if request.form:
|
||||
username = request.form.get('username')
|
||||
|
@ -44,7 +44,7 @@ def login():
|
|||
expires_at=expires)
|
||||
|
||||
if session:
|
||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
redirect_target = redirect(redirect_page, 302)
|
||||
response = make_response(redirect_target)
|
||||
response.set_cookie('session_token', session.session_token, expires=expires)
|
||||
return response
|
||||
|
|
|
@ -25,7 +25,7 @@ def logout():
|
|||
if not user:
|
||||
return abort(403, 'Invalid session token')
|
||||
|
||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
redirect_target = redirect(redirect_page, 302)
|
||||
response = make_response(redirect_target)
|
||||
response.set_cookie('session_token', '', expires=0)
|
||||
return response
|
||||
|
|
|
@ -45,12 +45,14 @@ def get_args(kwargs):
|
|||
if k == 'resolution':
|
||||
v = json.loads('[{}]'.format(v))
|
||||
else:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
v = int(v)
|
||||
except (ValueError, TypeError):
|
||||
except:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
v = float(v)
|
||||
except (ValueError, TypeError):
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs[k] = v
|
||||
|
|
|
@ -32,8 +32,8 @@ def add_media():
|
|||
args = {}
|
||||
try:
|
||||
args = json.loads(request.data.decode('utf-8'))
|
||||
except Exception as e:
|
||||
abort(400, 'Invalid JSON request: {}'.format(str(e)))
|
||||
except:
|
||||
abort(400, 'Invalid JSON request')
|
||||
|
||||
source = args.get('source')
|
||||
if not source:
|
||||
|
|
|
@ -40,7 +40,7 @@ def audio_feed(device, fifo, sample_rate, blocksize, latency, channels):
|
|||
channels=channels)
|
||||
|
||||
try:
|
||||
with open(fifo, 'rb') as f: # lgtm [py/path-injection]
|
||||
with open(fifo, 'rb') as f:
|
||||
send_header = True
|
||||
|
||||
while True:
|
||||
|
|
|
@ -31,10 +31,10 @@ def register():
|
|||
if session_token:
|
||||
user, session = user_manager.authenticate_user_session(session_token)
|
||||
if user:
|
||||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
return redirect(redirect_page, 302)
|
||||
|
||||
if user_manager.get_user_count() > 0:
|
||||
return redirect('/login?redirect=' + redirect_page, 302) # lgtm [py/url-redirection]
|
||||
return redirect('/login?redirect=' + redirect_page, 302)
|
||||
|
||||
if request.form:
|
||||
username = request.form.get('username')
|
||||
|
@ -49,7 +49,7 @@ def register():
|
|||
if not remember else None)
|
||||
|
||||
if session:
|
||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
redirect_target = redirect(redirect_page, 302)
|
||||
response = make_response(redirect_target)
|
||||
response.set_cookie('session_token', session.session_token)
|
||||
return response
|
||||
|
|
|
@ -123,8 +123,7 @@ def _authenticate_token():
|
|||
try:
|
||||
user_manager.validate_jwt_token(user_token)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger().debug(str(e))
|
||||
except:
|
||||
return token and user_token == token
|
||||
|
||||
|
||||
|
|
|
@ -78,11 +78,11 @@ class HttpRequest(object):
|
|||
|
||||
def get_new_items(self, response):
|
||||
""" Gets new items out of a response """
|
||||
raise NotImplementedError("get_new_items must be implemented in a derived class")
|
||||
raise ("get_new_items must be implemented in a derived class")
|
||||
|
||||
def __iter__(self):
|
||||
for (key, value) in self.request_args.items():
|
||||
yield key, value
|
||||
yield (key, value)
|
||||
|
||||
|
||||
class JsonHttpRequest(HttpRequest):
|
||||
|
@ -96,7 +96,7 @@ class JsonHttpRequest(HttpRequest):
|
|||
new_entries = []
|
||||
|
||||
if self.path:
|
||||
m = re.match(r'\${\s*(.*)\s*}', self.path)
|
||||
m = re.match('\$\{\s*(.*)\s*\}', self.path)
|
||||
response = eval(m.group(1))
|
||||
|
||||
for entry in response:
|
||||
|
|
|
@ -238,15 +238,15 @@ class RssUpdates(HttpRequest):
|
|||
with open(digest_filename, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
elif self.digest_format == 'pdf':
|
||||
from weasyprint import HTML, CSS
|
||||
import weasyprint
|
||||
from weasyprint.fonts import FontConfiguration
|
||||
|
||||
body_style = 'body { ' + self.body_style + ' }'
|
||||
body_style = 'body { {body_style} }'.format(body_style=self.body_style)
|
||||
font_config = FontConfiguration()
|
||||
css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'),
|
||||
CSS(string=body_style, font_config=font_config)]
|
||||
css = [weasyprint.CSS('https://fonts.googleapis.com/css?family=Merriweather'),
|
||||
weasyprint.CSS(string=body_style, font_config=font_config)]
|
||||
|
||||
HTML(string=content).write_pdf(digest_filename, stylesheets=css)
|
||||
weasyprint.HTML(string=content).write_pdf(digest_filename, stylesheets=css)
|
||||
else:
|
||||
raise RuntimeError('Unsupported format: {}. Supported formats: ' +
|
||||
'html or pdf'.format(self.digest_format))
|
||||
|
|
|
@ -50,13 +50,15 @@ class HttpUtils(object):
|
|||
for name, resource_path in resource_dirs.items():
|
||||
resource_path = os.path.abspath(os.path.expanduser(resource_path))
|
||||
if directory.startswith(resource_path):
|
||||
subdir = re.sub('^{}(.*)$'.format(resource_path),
|
||||
'\\1', directory)
|
||||
uri = '/resources/' + name
|
||||
break
|
||||
|
||||
if not uri:
|
||||
raise RuntimeError(('Directory {} not found among the available ' +
|
||||
'static resources on the webserver').format(
|
||||
directory))
|
||||
'static resources on the webserver').format(
|
||||
directory))
|
||||
|
||||
results = [
|
||||
re.sub('^{}(.*)$'.format(resource_path), uri + '\\1', path)
|
||||
|
@ -90,11 +92,10 @@ class HttpUtils(object):
|
|||
|
||||
@classmethod
|
||||
def plugin_name_to_tag(cls, module_name):
|
||||
return module_name.replace('.', '-')
|
||||
return module_name.replace('.','-')
|
||||
|
||||
@classmethod
|
||||
def find_templates_in_dir(cls, directory):
|
||||
# noinspection PyTypeChecker
|
||||
return [
|
||||
os.path.join(directory, file)
|
||||
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
45
platypush/backend/http/webapp/dist/static/css/app.9fba5ac9.css
vendored
Normal file
45
platypush/backend/http/webapp/dist/static/css/app.9fba5ac9.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-0021f7ee.0a2f05d9.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-0021f7ee.0a2f05d9.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-01c1b3b0.6bf22fce.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-01c1b3b0.6bf22fce.css
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
15
platypush/backend/http/webapp/dist/static/css/chunk-052643ba.b0333423.css
vendored
Normal file
15
platypush/backend/http/webapp/dist/static/css/chunk-052643ba.b0333423.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-07773226.3ce5e818.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-07773226.3ce5e818.css
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/css/chunk-0827360a.54ee8b20.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-0827360a.54ee8b20.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-09eaa919.38a05394.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-09eaa919.38a05394.css
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
21
platypush/backend/http/webapp/dist/static/css/chunk-167662ae.33531ad8.css
vendored
Normal file
21
platypush/backend/http/webapp/dist/static/css/chunk-167662ae.33531ad8.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-24b41efa.d1c66125.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-24b41efa.d1c66125.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-31bc5041.49496a07.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-31bc5041.49496a07.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-328f0535.a7fa242b.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-328f0535.a7fa242b.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-3b44ec4e.37d24614.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-3b44ec4e.37d24614.css
vendored
Normal file
File diff suppressed because one or more lines are too long
5
platypush/backend/http/webapp/dist/static/css/chunk-3e2d0b3f.a68561be.css
vendored
Normal file
5
platypush/backend/http/webapp/dist/static/css/chunk-3e2d0b3f.a68561be.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-40ee55e4.58afedb7.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-40ee55e4.58afedb7.css
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
3
platypush/backend/http/webapp/dist/static/css/chunk-437beeb4.1582e2d0.css
vendored
Normal file
3
platypush/backend/http/webapp/dist/static/css/chunk-437beeb4.1582e2d0.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-57f8147d.b22779c7.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-57f8147d.b22779c7.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
platypush/backend/http/webapp/dist/static/css/chunk-5b993de9.90be868a.css
vendored
Normal file
15
platypush/backend/http/webapp/dist/static/css/chunk-5b993de9.90be868a.css
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/css/chunk-5d632024.4e589a50.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-5d632024.4e589a50.css
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/css/chunk-60dbbc82.55f7ede0.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-60dbbc82.55f7ede0.css
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
7
platypush/backend/http/webapp/dist/static/css/chunk-61bbbfec.7f8a74cc.css
vendored
Normal file
7
platypush/backend/http/webapp/dist/static/css/chunk-61bbbfec.7f8a74cc.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-690ef4aa.6711f0b2.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-690ef4aa.6711f0b2.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
platypush/backend/http/webapp/dist/static/css/chunk-6ca52dd4.2a852a62.css
vendored
Normal file
11
platypush/backend/http/webapp/dist/static/css/chunk-6ca52dd4.2a852a62.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-75e68c24.397c4e62.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-75e68c24.397c4e62.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-7b23e138.83cde918.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-7b23e138.83cde918.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-9684cd10.7cdaa500.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-9684cd10.7cdaa500.css
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
1
platypush/backend/http/webapp/dist/static/css/chunk-972487d6.f475a218.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-972487d6.f475a218.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/chunk-9f884670.a37063a7.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/chunk-9f884670.a37063a7.css
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
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue