Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
a1791bcd80 | |||
008fea5197 |
1
.gitignore
vendored
|
@ -18,4 +18,5 @@ platypush/notebooks
|
||||||
platypush/requests
|
platypush/requests
|
||||||
/http-client.env.json
|
/http-client.env.json
|
||||||
/platypush/backend/http/static/css/dist
|
/platypush/backend/http/static/css/dist
|
||||||
|
/tests/etc/scripts
|
||||||
/tests/etc/dashboards
|
/tests/etc/dashboards
|
||||||
|
|
7
.readthedocs.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
build:
|
||||||
|
image: latest
|
||||||
|
|
||||||
|
python:
|
||||||
|
version: 3.7
|
||||||
|
setup_py_install: true
|
||||||
|
|
12
.travis.requirements
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
pyyaml
|
||||||
|
requests
|
||||||
|
flask
|
||||||
|
redis
|
||||||
|
python-dateutil
|
||||||
|
websockets
|
||||||
|
bcrypt
|
||||||
|
sqlalchemy
|
||||||
|
croniter
|
||||||
|
zeroconf>=0.27.0
|
||||||
|
pyjwt
|
||||||
|
pytest
|
22
.travis.yml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
language: python
|
||||||
|
dist: xenial
|
||||||
|
python:
|
||||||
|
- "3.7"
|
||||||
|
|
||||||
|
install: "pip install -r .travis.requirements"
|
||||||
|
|
||||||
|
script: pytest
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
recipients:
|
||||||
|
- blacklight86@gmail.com
|
||||||
|
on_success: change
|
||||||
|
on_failure: change
|
||||||
|
|
||||||
|
services:
|
||||||
|
- redis-server
|
||||||
|
|
||||||
|
git:
|
||||||
|
submodules: false
|
||||||
|
|
143
CHANGELOG.md
|
@ -3,149 +3,6 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
||||||
|
|
||||||
## [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
|
|
||||||
(see [#167](https://git.platypush.tech/platypush/platypush/-/issues/167)).
|
|
||||||
|
|
||||||
- Added `file.monitor` backend, which replaces the `inotify` backend
|
|
||||||
(see [#172](https://git.platypush.tech/platypush/platypush/-/issues/172)).
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- Added support for a static list of devices to actively scan to the `bluetooth.scanner` backend
|
|
||||||
(see [#174](https://git.platypush.tech/platypush/platypush/-/issues/174)).
|
|
||||||
|
|
||||||
- Added `weather.openweathermap` plugin and backend, which replaces `weather.darksky`, since the
|
|
||||||
Darksky API will be completely shut down by the end of 2021.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Cron expressions should adhere to the UNIX cronjob standard and use the machine local time,
|
|
||||||
not UTC, as a reference (closes [#173](https://git.platypush.tech/platypush/platypush/-/issues/173)).
|
|
||||||
|
|
||||||
- Better management of Z-Wave values types from the UI.
|
|
||||||
|
|
||||||
- Disable logging for `ZwaveValueEvent` events - they tend to be very verbose and
|
|
||||||
can impact the performance on slower devices. They will still be published to the
|
|
||||||
websocket clients though, so you can still debug Z-Wave values issues from the browser
|
|
||||||
developer console (enable debug traces).
|
|
||||||
|
|
||||||
- Added suffix to the `zigbee.mqtt` backend default `client_id` to prevent clashes with
|
|
||||||
the default `mqtt` backend `client_id`.
|
|
||||||
|
|
||||||
## [0.20.4] - 2021-03-08
|
## [0.20.4] - 2021-03-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
Thanks for considering contributing your work to make Platypush a better product!
|
|
||||||
|
|
||||||
Contributions are more than welcome, and the follow the standard Gitlab procedure:
|
|
||||||
|
|
||||||
- [Fork the repo](https://git.platypush.tech/platypush/platypush).
|
|
||||||
- Prepare your changes.
|
|
||||||
- [Submit a merge request](https://git.platypush.tech/platypush/platypush/-/merge_requests).
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
|
|
||||||
- The code should ideally have no LINT warnings/issues.
|
|
||||||
|
|
||||||
- Project conventions:
|
|
||||||
- 4 spaces to indent.
|
|
||||||
- RST format for classes and methods documentation
|
|
||||||
- Run `python generate_missing_docs.py` if you are adding new plugins/backends to automatically
|
|
||||||
generate the doc templates. Make sure that you don't accidentally remove lines elements from
|
|
||||||
the docs because of missing dependencies on the machine you use to generate the docs.
|
|
||||||
- Naming conventions: plugin classes are named `<Module>Plugin` and backend classes are
|
|
||||||
named `<Module>Backend`, with `<Module>` being the (camel-case) representation of the
|
|
||||||
Python module of the plugin without the prefix - for example, the plugin under
|
|
||||||
`platypush.plugins.light.hue` must be named `LightHuePlugin`.
|
|
||||||
|
|
||||||
- If possible, [add a test](https://git.platypush.tech/platypush/platypush/-/tree/master/tests)
|
|
||||||
for the new functionality. If you have built a new functionality that works with some specific
|
|
||||||
device or service then it's not required to write a test that mocks the whole service, but if
|
|
||||||
you are changing some of the core entities (e.g. requests, events, procedures, hooks, crons
|
|
||||||
or the bus) then make sure to add tests and not to break the existing tests.
|
|
||||||
|
|
||||||
- If the feature requires an optional dependency then make sure to document it:
|
|
||||||
|
|
||||||
- In the class docstring (see other plugins and backends for examples)
|
|
||||||
- In [`setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72) as
|
|
||||||
an `extras_require` entry
|
|
||||||
- In [`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt) -
|
|
||||||
if the feature is optional then leave it commented and add a one-line comment to explain which
|
|
||||||
plugin or backend requires it.
|
|
60
README.md
|
@ -1,14 +1,12 @@
|
||||||
Platypush
|
Platypush
|
||||||
=========
|
=========
|
||||||
|
|
||||||
[](https://ci.platypush.tech/latest.log)
|
[](https://ci.platypush.tech)
|
||||||
[](https://ci.platypush.tech/docs/latest.log)
|
[](https://docs.platypush.tech/en/latest/)
|
||||||
[](https://pypi.python.org/pypi/platypush/)
|
[](https://pypi.python.org/pypi/platypush/)
|
||||||
[](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
|
[](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
|
||||||
[](https://git.platypush.tech/platypush/platypush/-/commits/master/)
|
[](https://git.platypush.tech/platypush/platypush/-/commits/master/)
|
||||||
[](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
|
[](https://git.platypush.tech/platypush/platypush/-/issues)
|
||||||
[](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
|
|
||||||
[](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).
|
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
|
||||||
|
|
||||||
|
@ -16,7 +14,7 @@ Platypush
|
||||||
|
|
||||||
- The [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
|
- The [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
|
||||||
|
|
||||||
- Extensive documentation for all the available integrations and messages [is available](https://docs.platypush.tech/).
|
- Extensive documentation for all the available integrations and messages is available on [ReadTheDocs](https://docs.platypush.tech/en/latest/).
|
||||||
|
|
||||||
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
|
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
|
||||||
|
|
||||||
|
@ -279,48 +277,6 @@ curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_
|
||||||
}' http://host:8008/execute
|
}' http://host:8008/execute
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cronjobs
|
|
||||||
|
|
||||||
Cronjobs are pieces of logic that will be run at regular intervals, expressed in crontab-compatible syntax.
|
|
||||||
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` as
|
|
||||||
functions labelled by the `@cron` decorator.
|
|
||||||
|
|
||||||
Note that seconds are also supported (unlike the standard crontab definition), but, for back-compatibility with the
|
|
||||||
standard crontab format, they are at the end of the cron expression, so the expression is actually in the format
|
|
||||||
`<minute> <hour> <day_of_month> <month> <day_of_week> <second>`.
|
|
||||||
|
|
||||||
YAML example for a cronjob that is executed every 30 seconds and checks if a Bluetooth device is nearby:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
cron.check_bt_device:
|
|
||||||
cron_expression: '* * * * * */30'
|
|
||||||
actions:
|
|
||||||
- action: bluetooth.lookup_name
|
|
||||||
args:
|
|
||||||
addr: XX:XX:XX:XX:XX:XX
|
|
||||||
|
|
||||||
- if ${name}:
|
|
||||||
- action: procedure.on_device_on
|
|
||||||
- else:
|
|
||||||
- action: procedure.on_device_off
|
|
||||||
```
|
|
||||||
|
|
||||||
Python example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Content of ~/.config/platypush/scripts/bt_cron.py
|
|
||||||
from platypush.cron import cron
|
|
||||||
from platypush.utils import run
|
|
||||||
|
|
||||||
@cron('* * * * * */30')
|
|
||||||
def check_bt_device(**context):
|
|
||||||
name = run('bluetooth.lookup_name').get('name')
|
|
||||||
if name:
|
|
||||||
# on_device_on logic here
|
|
||||||
else:
|
|
||||||
# on_device_off logic here
|
|
||||||
```
|
|
||||||
|
|
||||||
### The web interface
|
### The web interface
|
||||||
|
|
||||||
If [`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html) is enabled then a web interface
|
If [`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html) is enabled then a web interface
|
||||||
|
@ -476,15 +432,9 @@ platydock stop device_id
|
||||||
4. Remove the instance:
|
4. Remove the instance:
|
||||||
|
|
||||||
```shell
|
```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
|
## Tests
|
||||||
|
|
||||||
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
|
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Platypush self-generated reference
|
|
||||||
====================================
|
|
||||||
|
|
||||||
This directory contains the Sphinx self-generated documentation for Platypush.
|
|
||||||
|
|
||||||
Dependencies required to generate the documentation:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ [sudo] pip install sphinx 'git+https://github.com/bashtage/sphinx-material.git'
|
|
||||||
```
|
|
||||||
|
|
||||||
To generate the HTML documentation:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ make html
|
|
||||||
```
|
|
||||||
|
|
||||||
The output will be generated under `build/html`.
|
|
||||||
|
|
||||||
Type `make` with no additional arguments to get a full list of the supported output formats.
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -22,7 +22,6 @@ Backends
|
||||||
platypush/backend/clipboard.rst
|
platypush/backend/clipboard.rst
|
||||||
platypush/backend/covid19.rst
|
platypush/backend/covid19.rst
|
||||||
platypush/backend/dbus.rst
|
platypush/backend/dbus.rst
|
||||||
platypush/backend/file.monitor.rst
|
|
||||||
platypush/backend/foursquare.rst
|
platypush/backend/foursquare.rst
|
||||||
platypush/backend/github.rst
|
platypush/backend/github.rst
|
||||||
platypush/backend/google.fit.rst
|
platypush/backend/google.fit.rst
|
||||||
|
@ -32,19 +31,16 @@ Backends
|
||||||
platypush/backend/http.poll.rst
|
platypush/backend/http.poll.rst
|
||||||
platypush/backend/inotify.rst
|
platypush/backend/inotify.rst
|
||||||
platypush/backend/joystick.rst
|
platypush/backend/joystick.rst
|
||||||
platypush/backend/joystick.jstest.rst
|
|
||||||
platypush/backend/joystick.linux.rst
|
|
||||||
platypush/backend/kafka.rst
|
platypush/backend/kafka.rst
|
||||||
platypush/backend/light.hue.rst
|
platypush/backend/light.hue.rst
|
||||||
platypush/backend/linode.rst
|
platypush/backend/linode.rst
|
||||||
platypush/backend/log.http.rst
|
platypush/backend/local.rst
|
||||||
platypush/backend/mail.rst
|
platypush/backend/mail.rst
|
||||||
platypush/backend/midi.rst
|
platypush/backend/midi.rst
|
||||||
platypush/backend/mqtt.rst
|
platypush/backend/mqtt.rst
|
||||||
platypush/backend/music.mopidy.rst
|
platypush/backend/music.mopidy.rst
|
||||||
platypush/backend/music.mpd.rst
|
platypush/backend/music.mpd.rst
|
||||||
platypush/backend/music.snapcast.rst
|
platypush/backend/music.snapcast.rst
|
||||||
platypush/backend/music.spotify.connect.rst
|
|
||||||
platypush/backend/nextcloud.rst
|
platypush/backend/nextcloud.rst
|
||||||
platypush/backend/nfc.rst
|
platypush/backend/nfc.rst
|
||||||
platypush/backend/nodered.rst
|
platypush/backend/nodered.rst
|
||||||
|
@ -75,12 +71,9 @@ Backends
|
||||||
platypush/backend/todoist.rst
|
platypush/backend/todoist.rst
|
||||||
platypush/backend/travisci.rst
|
platypush/backend/travisci.rst
|
||||||
platypush/backend/trello.rst
|
platypush/backend/trello.rst
|
||||||
platypush/backend/weather.rst
|
|
||||||
platypush/backend/weather.buienradar.rst
|
platypush/backend/weather.buienradar.rst
|
||||||
platypush/backend/weather.darksky.rst
|
platypush/backend/weather.darksky.rst
|
||||||
platypush/backend/weather.openweathermap.rst
|
|
||||||
platypush/backend/websocket.rst
|
platypush/backend/websocket.rst
|
||||||
platypush/backend/wiimote.rst
|
platypush/backend/wiimote.rst
|
||||||
platypush/backend/zigbee.mqtt.rst
|
platypush/backend/zigbee.mqtt.rst
|
||||||
platypush/backend/zwave.rst
|
platypush/backend/zwave.rst
|
||||||
platypush/backend/zwave.mqtt.rst
|
|
||||||
|
|
|
@ -18,13 +18,12 @@ import sys
|
||||||
# import os
|
# import os
|
||||||
# import sys
|
# import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
sys.path.insert(0, os.path.abspath("./_ext"))
|
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'Platypush'
|
project = 'platypush'
|
||||||
copyright = '2017-2021, Fabio Manganiello'
|
copyright = '2017-2020, Fabio Manganiello'
|
||||||
author = 'Fabio Manganiello'
|
author = 'Fabio Manganiello'
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
|
@ -51,7 +50,6 @@ extensions = [
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
'sphinx.ext.githubpages',
|
'sphinx.ext.githubpages',
|
||||||
'sphinx_rtd_theme',
|
'sphinx_rtd_theme',
|
||||||
'sphinx_marshmallow',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
@ -88,8 +86,7 @@ pygments_style = 'sphinx'
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
# html_theme = 'haiku'
|
# html_theme = 'haiku'
|
||||||
# html_theme = 'sphinx_rtd_theme'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
html_theme = 'sphinx_material'
|
|
||||||
|
|
||||||
html_domain_indices = True
|
html_domain_indices = True
|
||||||
|
|
||||||
|
@ -97,52 +94,7 @@ html_domain_indices = True
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
#
|
#
|
||||||
html_theme_options = {
|
# html_theme_options = {}
|
||||||
'nav_title': 'Platypush documentation',
|
|
||||||
'repo_url': 'https://git.platypush.tech/platypush/platypush',
|
|
||||||
'repo_name': 'Source code',
|
|
||||||
'repo_type': 'gitlab',
|
|
||||||
'color_primary': 'green',
|
|
||||||
'color_accent': 'light-green',
|
|
||||||
'logo_icon': '🕮',
|
|
||||||
'nav_links': [
|
|
||||||
{
|
|
||||||
'href': 'https://platypush.tech/',
|
|
||||||
'title': 'Homepage',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://blog.platypush.tech/',
|
|
||||||
'title': 'Blog',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://git.platypush.tech/platypush/platypush',
|
|
||||||
'title': 'Repository',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://git.platypush.tech/platypush/platypush/-/wikis/home',
|
|
||||||
'title': 'Wiki',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://chrome.google.com/webstore/detail/platypush/aphldjclndofhflbbdnmpejbjgomkbie',
|
|
||||||
'title': 'Chrome Extension',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://addons.mozilla.org/en-US/firefox/addon/platypush/',
|
|
||||||
'title': 'Firefox Extension',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://f-droid.org/en/packages/tech.platypush.platypush/',
|
|
||||||
'title': 'Android App',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
@ -157,9 +109,8 @@ html_theme_options = {
|
||||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||||
# 'searchbox.html']``.
|
# 'searchbox.html']``.
|
||||||
#
|
#
|
||||||
html_sidebars = {
|
# html_sidebars = {}
|
||||||
'**': ['logo-text.html', 'globaltoc.html', 'localtoc.html', 'searchbox.html']
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ---------------------------------------------
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
|
@ -314,7 +265,6 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'imapclient',
|
'imapclient',
|
||||||
'pysmartthings',
|
'pysmartthings',
|
||||||
'aiohttp',
|
'aiohttp',
|
||||||
'watchdog',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
@ -327,5 +277,3 @@ def skip(app, what, name, obj, skip, options):
|
||||||
def setup(app):
|
def setup(app):
|
||||||
app.connect("autodoc-skip-member", skip)
|
app.connect("autodoc-skip-member", skip)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ Events
|
||||||
platypush/events/covid19.rst
|
platypush/events/covid19.rst
|
||||||
platypush/events/custom.rst
|
platypush/events/custom.rst
|
||||||
platypush/events/distance.rst
|
platypush/events/distance.rst
|
||||||
platypush/events/file.rst
|
|
||||||
platypush/events/foursquare.rst
|
platypush/events/foursquare.rst
|
||||||
platypush/events/geo.rst
|
platypush/events/geo.rst
|
||||||
platypush/events/github.rst
|
platypush/events/github.rst
|
||||||
|
@ -28,13 +27,13 @@ Events
|
||||||
platypush/events/gps.rst
|
platypush/events/gps.rst
|
||||||
platypush/events/http.rst
|
platypush/events/http.rst
|
||||||
platypush/events/http.hook.rst
|
platypush/events/http.hook.rst
|
||||||
|
platypush/events/http.ota.booking.rst
|
||||||
platypush/events/http.rss.rst
|
platypush/events/http.rss.rst
|
||||||
platypush/events/inotify.rst
|
platypush/events/inotify.rst
|
||||||
platypush/events/joystick.rst
|
platypush/events/joystick.rst
|
||||||
platypush/events/kafka.rst
|
platypush/events/kafka.rst
|
||||||
platypush/events/light.rst
|
platypush/events/light.rst
|
||||||
platypush/events/linode.rst
|
platypush/events/linode.rst
|
||||||
platypush/events/log.http.rst
|
|
||||||
platypush/events/mail.rst
|
platypush/events/mail.rst
|
||||||
platypush/events/media.rst
|
platypush/events/media.rst
|
||||||
platypush/events/midi.rst
|
platypush/events/midi.rst
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.file.monitor``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.file.monitor
|
|
||||||
:members:
|
|
|
@ -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:
|
|
5
docs/source/platypush/backend/local.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.backend.local``
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. automodule:: platypush.backend.local
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.log.http``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.log.http
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.music.spotify.connect``
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.music.spotify.connect
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.weather.openweathermap``
|
|
||||||
============================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.weather.openweathermap
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.weather``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.weather
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.zwave.mqtt``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.zwave.mqtt
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.file``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.file
|
|
||||||
:members:
|
|
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:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.log.http``
|
|
||||||
====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.log.http
|
|
||||||
: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
|
@ -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
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.video.torrentcast``
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.video.torrentcast
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.weather.openweathermap``
|
|
||||||
============================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.weather.openweathermap
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.weather``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.weather
|
|
||||||
: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/graphite.rst
|
||||||
platypush/plugins/homeseer.rst
|
platypush/plugins/homeseer.rst
|
||||||
platypush/plugins/http.request.rst
|
platypush/plugins/http.request.rst
|
||||||
|
platypush/plugins/http.request.ota.booking.rst
|
||||||
platypush/plugins/http.request.rss.rst
|
platypush/plugins/http.request.rss.rst
|
||||||
platypush/plugins/http.webpage.rst
|
platypush/plugins/http.webpage.rst
|
||||||
platypush/plugins/ifttt.rst
|
platypush/plugins/ifttt.rst
|
||||||
|
@ -104,7 +105,6 @@ Plugins
|
||||||
platypush/plugins/ping.rst
|
platypush/plugins/ping.rst
|
||||||
platypush/plugins/printer.cups.rst
|
platypush/plugins/printer.cups.rst
|
||||||
platypush/plugins/pushbullet.rst
|
platypush/plugins/pushbullet.rst
|
||||||
platypush/plugins/pwm.pca9685.rst
|
|
||||||
platypush/plugins/qrcode.rst
|
platypush/plugins/qrcode.rst
|
||||||
platypush/plugins/redis.rst
|
platypush/plugins/redis.rst
|
||||||
platypush/plugins/rtorrent.rst
|
platypush/plugins/rtorrent.rst
|
||||||
|
@ -119,10 +119,9 @@ Plugins
|
||||||
platypush/plugins/stt.picovoice.hotword.rst
|
platypush/plugins/stt.picovoice.hotword.rst
|
||||||
platypush/plugins/stt.picovoice.speech.rst
|
platypush/plugins/stt.picovoice.speech.rst
|
||||||
platypush/plugins/switch.rst
|
platypush/plugins/switch.rst
|
||||||
|
platypush/plugins/switch.switchbot.rst
|
||||||
platypush/plugins/switch.tplink.rst
|
platypush/plugins/switch.tplink.rst
|
||||||
platypush/plugins/switch.wemo.rst
|
platypush/plugins/switch.wemo.rst
|
||||||
platypush/plugins/switchbot.rst
|
|
||||||
platypush/plugins/switchbot.bluetooth.rst
|
|
||||||
platypush/plugins/system.rst
|
platypush/plugins/system.rst
|
||||||
platypush/plugins/tcp.rst
|
platypush/plugins/tcp.rst
|
||||||
platypush/plugins/tensorflow.rst
|
platypush/plugins/tensorflow.rst
|
||||||
|
@ -138,14 +137,11 @@ Plugins
|
||||||
platypush/plugins/user.rst
|
platypush/plugins/user.rst
|
||||||
platypush/plugins/utils.rst
|
platypush/plugins/utils.rst
|
||||||
platypush/plugins/variable.rst
|
platypush/plugins/variable.rst
|
||||||
platypush/plugins/weather.rst
|
platypush/plugins/video.torrentcast.rst
|
||||||
platypush/plugins/weather.buienradar.rst
|
platypush/plugins/weather.buienradar.rst
|
||||||
platypush/plugins/weather.darksky.rst
|
platypush/plugins/weather.darksky.rst
|
||||||
platypush/plugins/weather.openweathermap.rst
|
|
||||||
platypush/plugins/websocket.rst
|
platypush/plugins/websocket.rst
|
||||||
platypush/plugins/wiimote.rst
|
platypush/plugins/wiimote.rst
|
||||||
platypush/plugins/zeroconf.rst
|
platypush/plugins/zeroconf.rst
|
||||||
platypush/plugins/zigbee.mqtt.rst
|
platypush/plugins/zigbee.mqtt.rst
|
||||||
platypush/plugins/zwave.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>'
|
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
||||||
__version__ = '0.21.1'
|
__version__ = '0.20.4'
|
||||||
|
|
||||||
logger = logging.getLogger('platypush')
|
logger = logging.getLogger('platypush')
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from threading import Thread, Event as ThreadEvent, get_ident
|
from threading import Thread
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict
|
||||||
|
|
||||||
from platypush.bus import Bus
|
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.poll_seconds = float(poll_seconds) if poll_seconds else None
|
||||||
self.device_id = Config.get('device_id')
|
self.device_id = Config.get('device_id')
|
||||||
self.thread_id = None
|
self.thread_id = None
|
||||||
self._stop_event = ThreadEvent()
|
self._stop_event = threading.Event()
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
|
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
|
||||||
self.zeroconf = None
|
self.zeroconf = None
|
||||||
|
@ -219,7 +220,7 @@ class Backend(Thread, EventGenerator):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
""" Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """
|
""" 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)
|
set_thread_name(self._thread_name)
|
||||||
if not callable(self.loop):
|
if not callable(self.loop):
|
||||||
return
|
return
|
||||||
|
|
|
@ -65,11 +65,11 @@ class AdafruitIoBackend(Backend):
|
||||||
def on_message(self, msg):
|
def on_message(self, msg):
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
def _handler(client, feed, data):
|
def _handler(client, feed, data):
|
||||||
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
data = float(data)
|
data = float(data)
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.debug('Not a number: {}: {}'.format(data, e))
|
pass
|
||||||
|
|
||||||
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
|
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
|
||||||
|
|
||||||
return _handler
|
return _handler
|
||||||
|
|
|
@ -55,17 +55,16 @@ class Alarm:
|
||||||
self._runtime_snooze_interval = snooze_interval
|
self._runtime_snooze_interval = snooze_interval
|
||||||
|
|
||||||
def get_next(self) -> float:
|
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:
|
try:
|
||||||
cron = croniter.croniter(self.when, now)
|
cron = croniter.croniter(self.when, now)
|
||||||
return cron.get_next()
|
return cron.get_next()
|
||||||
except (AttributeError, croniter.CroniterBadCronError):
|
except (AttributeError, croniter.CroniterBadCronError):
|
||||||
try:
|
try:
|
||||||
timestamp = datetime.datetime.fromisoformat(self.when).replace(
|
timestamp = datetime.datetime.fromisoformat(self.when).replace(tzinfo=gettz())
|
||||||
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
|
||||||
except (TypeError, ValueError):
|
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)))
|
datetime.timedelta(seconds=int(self.when)))
|
||||||
|
|
||||||
return timestamp.timestamp() if timestamp >= now else None
|
return timestamp.timestamp() if timestamp >= now else None
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import time
|
from typing import Dict, Optional
|
||||||
from threading import Thread, RLock
|
|
||||||
from typing import Dict, Optional, List
|
|
||||||
|
|
||||||
from platypush.backend.sensor import SensorBackend
|
from platypush.backend.sensor import SensorBackend
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
|
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,12 +20,10 @@ class BluetoothScannerBackend(SensorBackend):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10,
|
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10, **kwargs):
|
||||||
track_devices: Optional[List[str]] = None, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
|
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
|
||||||
:param scan_duration: How long the scan should run (default: 10 seconds).
|
:param scan_duration: How long the scan should run (default: 10 seconds).
|
||||||
:param track_devices: List of addresses of devices to actively track, even if they aren't discoverable.
|
|
||||||
"""
|
"""
|
||||||
super().__init__(plugin='bluetooth', plugin_args={
|
super().__init__(plugin='bluetooth', plugin_args={
|
||||||
'device_id': device_id,
|
'device_id': device_id,
|
||||||
|
@ -36,72 +31,17 @@ class BluetoothScannerBackend(SensorBackend):
|
||||||
}, **kwargs)
|
}, **kwargs)
|
||||||
|
|
||||||
self._last_seen_devices = {}
|
self._last_seen_devices = {}
|
||||||
self._tracking_thread: Optional[Thread] = None
|
|
||||||
self._bt_lock = RLock()
|
|
||||||
self.track_devices = set(track_devices or [])
|
|
||||||
self.scan_duration = scan_duration
|
|
||||||
|
|
||||||
def _add_last_seen_device(self, dev):
|
|
||||||
addr = dev.pop('addr')
|
|
||||||
if addr not in self._last_seen_devices:
|
|
||||||
self.bus.post(BluetoothDeviceFoundEvent(address=addr, **dev))
|
|
||||||
self._last_seen_devices[addr] = {'addr': addr, **dev}
|
|
||||||
|
|
||||||
def _remove_last_seen_device(self, addr: str):
|
|
||||||
dev = self._last_seen_devices.get(addr)
|
|
||||||
if not dev:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.bus.post(BluetoothDeviceLostEvent(address=addr, **dev))
|
|
||||||
del self._last_seen_devices[addr]
|
|
||||||
|
|
||||||
def _addr_tracker(self, addr):
|
|
||||||
with self._bt_lock:
|
|
||||||
name = get_plugin('bluetooth').lookup_name(addr, timeout=self.scan_duration).name
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
self._remove_last_seen_device(addr)
|
|
||||||
else:
|
|
||||||
self._add_last_seen_device({'addr': addr, 'name': name})
|
|
||||||
|
|
||||||
def _bt_tracker(self):
|
|
||||||
self.logger.info('Starting Bluetooth tracker')
|
|
||||||
while not self.should_stop():
|
|
||||||
trackers = []
|
|
||||||
for addr in self.track_devices:
|
|
||||||
tracker = Thread(target=self._addr_tracker, args=(addr,))
|
|
||||||
tracker.start()
|
|
||||||
trackers.append(tracker)
|
|
||||||
|
|
||||||
for tracker in trackers:
|
|
||||||
tracker.join(timeout=self.scan_duration)
|
|
||||||
|
|
||||||
time.sleep(self.scan_duration)
|
|
||||||
|
|
||||||
self.logger.info('Bluetooth tracker stopped')
|
|
||||||
|
|
||||||
def get_measurement(self):
|
|
||||||
with self._bt_lock:
|
|
||||||
return super().get_measurement()
|
|
||||||
|
|
||||||
def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]):
|
def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]):
|
||||||
for addr, dev in data.items():
|
for addr, dev in data.items():
|
||||||
self._add_last_seen_device(dev)
|
if addr not in self._last_seen_devices:
|
||||||
|
self.bus.post(BluetoothDeviceFoundEvent(address=dev.pop('addr'), **dev))
|
||||||
|
self._last_seen_devices[addr] = {'addr': addr, **dev}
|
||||||
|
|
||||||
for addr, dev in self._last_seen_devices.copy().items():
|
for addr, dev in self._last_seen_devices.copy().items():
|
||||||
if addr not in data and addr not in self.track_devices:
|
if addr not in data:
|
||||||
self._remove_last_seen_device(addr)
|
self.bus.post(BluetoothDeviceLostEvent(address=dev.pop('addr'), **dev))
|
||||||
|
del self._last_seen_devices[addr]
|
||||||
def run(self):
|
|
||||||
self._tracking_thread = Thread(target=self._bt_tracker)
|
|
||||||
self._tracking_thread.start()
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
super().on_stop()
|
|
||||||
if self._tracking_thread and self._tracking_thread.is_alive():
|
|
||||||
self.logger.info('Waiting for the Bluetooth tracking thread to stop')
|
|
||||||
self._tracking_thread.join(timeout=self.scan_duration)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -13,41 +13,37 @@ Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
|
||||||
import asyncio
|
import asyncio
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
import time
|
||||||
import struct
|
import struct
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
class CreateConnectionChannelError(Enum):
|
class CreateConnectionChannelError(Enum):
|
||||||
NoError = 0
|
NoError = 0
|
||||||
MaxPendingConnectionsReached = 1
|
MaxPendingConnectionsReached = 1
|
||||||
|
|
||||||
|
|
||||||
class ConnectionStatus(Enum):
|
class ConnectionStatus(Enum):
|
||||||
Disconnected = 0
|
Disconnected = 0
|
||||||
Connected = 1
|
Connected = 1
|
||||||
Ready = 2
|
Ready = 2
|
||||||
|
|
||||||
|
|
||||||
class DisconnectReason(Enum):
|
class DisconnectReason(Enum):
|
||||||
Unspecified = 0
|
Unspecified = 0
|
||||||
ConnectionEstablishmentFailed = 1
|
ConnectionEstablishmentFailed = 1
|
||||||
TimedOut = 2
|
TimedOut = 2
|
||||||
BondingKeysMismatch = 3
|
BondingKeysMismatch = 3
|
||||||
|
|
||||||
|
|
||||||
class RemovedReason(Enum):
|
class RemovedReason(Enum):
|
||||||
RemovedByThisClient = 0
|
RemovedByThisClient = 0
|
||||||
ForceDisconnectedByThisClient = 1
|
ForceDisconnectedByThisClient = 1
|
||||||
ForceDisconnectedByOtherClient = 2
|
ForceDisconnectedByOtherClient = 2
|
||||||
|
|
||||||
ButtonIsPrivate = 3
|
ButtonIsPrivate = 3
|
||||||
VerifyTimeout = 4
|
VerifyTimeout = 4
|
||||||
InternetBackendError = 5
|
InternetBackendError = 5
|
||||||
InvalidData = 6
|
InvalidData = 6
|
||||||
|
|
||||||
CouldntLoadDevice = 7
|
CouldntLoadDevice = 7
|
||||||
|
|
||||||
|
|
||||||
class ClickType(Enum):
|
class ClickType(Enum):
|
||||||
ButtonDown = 0
|
ButtonDown = 0
|
||||||
ButtonUp = 1
|
ButtonUp = 1
|
||||||
|
@ -56,24 +52,20 @@ class ClickType(Enum):
|
||||||
ButtonDoubleClick = 4
|
ButtonDoubleClick = 4
|
||||||
ButtonHold = 5
|
ButtonHold = 5
|
||||||
|
|
||||||
|
|
||||||
class BdAddrType(Enum):
|
class BdAddrType(Enum):
|
||||||
PublicBdAddrType = 0
|
PublicBdAddrType = 0
|
||||||
RandomBdAddrType = 1
|
RandomBdAddrType = 1
|
||||||
|
|
||||||
|
|
||||||
class LatencyMode(Enum):
|
class LatencyMode(Enum):
|
||||||
NormalLatency = 0
|
NormalLatency = 0
|
||||||
LowLatency = 1
|
LowLatency = 1
|
||||||
HighLatency = 2
|
HighLatency = 2
|
||||||
|
|
||||||
|
|
||||||
class BluetoothControllerState(Enum):
|
class BluetoothControllerState(Enum):
|
||||||
Detached = 0
|
Detached = 0
|
||||||
Resetting = 1
|
Resetting = 1
|
||||||
Attached = 2
|
Attached = 2
|
||||||
|
|
||||||
|
|
||||||
class ScanWizardResult(Enum):
|
class ScanWizardResult(Enum):
|
||||||
WizardSuccess = 0
|
WizardSuccess = 0
|
||||||
WizardCancelledByUser = 1
|
WizardCancelledByUser = 1
|
||||||
|
@ -83,26 +75,24 @@ class ScanWizardResult(Enum):
|
||||||
WizardInternetBackendError = 5
|
WizardInternetBackendError = 5
|
||||||
WizardInvalidData = 6
|
WizardInvalidData = 6
|
||||||
|
|
||||||
|
|
||||||
class ButtonScanner:
|
class ButtonScanner:
|
||||||
"""ButtonScanner class.
|
"""ButtonScanner class.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
scanner = ButtonScanner()
|
scanner = ButtonScanner()
|
||||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
||||||
client.add_scanner(scanner)
|
client.add_scanner(scanner)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_id = next(ButtonScanner._cnt)
|
self._scan_id = next(ButtonScanner._cnt)
|
||||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
||||||
|
|
||||||
|
|
||||||
class ScanWizard:
|
class ScanWizard:
|
||||||
"""ScanWizard class
|
"""ScanWizard class
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
wizard = ScanWizard()
|
wizard = ScanWizard()
|
||||||
wizard.on_found_private_button = lambda scan_wizard: ...
|
wizard.on_found_private_button = lambda scan_wizard: ...
|
||||||
|
@ -111,9 +101,9 @@ class ScanWizard:
|
||||||
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
||||||
client.add_scan_wizard(wizard)
|
client.add_scan_wizard(wizard)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_wizard_id = next(ScanWizard._cnt)
|
self._scan_wizard_id = next(ScanWizard._cnt)
|
||||||
self._bd_addr = None
|
self._bd_addr = None
|
||||||
|
@ -123,34 +113,33 @@ class ScanWizard:
|
||||||
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
|
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
|
||||||
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
|
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
|
||||||
|
|
||||||
|
|
||||||
class ButtonConnectionChannel:
|
class ButtonConnectionChannel:
|
||||||
"""ButtonConnectionChannel class.
|
"""ButtonConnectionChannel class.
|
||||||
|
|
||||||
This class represents a connection channel to a Flic button.
|
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).
|
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.
|
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
|
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,
|
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
|
||||||
referencing this object.
|
referencing this object.
|
||||||
|
|
||||||
Available properties and the function parameters are:
|
Available properties and the function parameters are:
|
||||||
on_create_connection_channel_response: channel, error, connection_status
|
on_create_connection_channel_response: channel, error, connection_status
|
||||||
on_removed: channel, removed_reason
|
on_removed: channel, removed_reason
|
||||||
on_connection_status_changed: channel, connection_status, disconnect_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
|
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()
|
_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._conn_id = next(ButtonConnectionChannel._cnt)
|
||||||
self._bd_addr = bd_addr
|
self._bd_addr = bd_addr
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
||||||
self.on_removed = lambda channel, removed_reason: None
|
self.on_removed = lambda channel, removed_reason: None
|
||||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_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_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 = 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
|
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bd_addr(self):
|
def bd_addr(self):
|
||||||
return self._bd_addr
|
return self._bd_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latency_mode(self):
|
def latency_mode(self):
|
||||||
return self._latency_mode
|
return self._latency_mode
|
||||||
|
|
||||||
@latency_mode.setter
|
@latency_mode.setter
|
||||||
def latency_mode(self, latency_mode):
|
def latency_mode(self, latency_mode):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
return
|
return
|
||||||
|
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
self._client._send_command("CmdChangeModeParameters",
|
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
|
||||||
"auto_disconnect_time": self._auto_disconnect_time})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_disconnect_time(self):
|
def auto_disconnect_time(self):
|
||||||
return self._auto_disconnect_time
|
return self._auto_disconnect_time
|
||||||
|
|
||||||
@auto_disconnect_time.setter
|
@auto_disconnect_time.setter
|
||||||
def auto_disconnect_time(self, auto_disconnect_time):
|
def auto_disconnect_time(self, auto_disconnect_time):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
return
|
return
|
||||||
|
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
self._client._send_command("CmdChangeModeParameters",
|
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
|
||||||
"auto_disconnect_time": self._auto_disconnect_time})
|
|
||||||
|
|
||||||
|
|
||||||
class FlicClient(asyncio.Protocol):
|
class FlicClient(asyncio.Protocol):
|
||||||
"""FlicClient class.
|
"""FlicClient class.
|
||||||
|
|
||||||
When this class is constructed, a socket connection is established.
|
When this class is constructed, a socket connection is established.
|
||||||
You may then send commands to the server and set timers.
|
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.
|
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.
|
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 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.
|
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 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.
|
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):
|
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_new_verified_button: bd_addr
|
||||||
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_bluetooth_controller_state_change: state
|
on_bluetooth_controller_state_change: state
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_EVENTS = [
|
_EVENTS = [
|
||||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
||||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
("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"),
|
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||||
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||||
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
||||||
("EvtGetInfoResponse", "<B6sBBhBBH",
|
("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"),
|
||||||
"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"),
|
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||||
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||||
("EvtBluetoothControllerStateChange", "<B", "state"),
|
("EvtBluetoothControllerStateChange", "<B", "state"),
|
||||||
|
@ -240,9 +223,9 @@ class FlicClient(asyncio.Protocol):
|
||||||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
||||||
]
|
]
|
||||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _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 is None else namedtuple(x[0], x[2]), _EVENTS))
|
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
|
||||||
|
|
||||||
_COMMANDS = [
|
_COMMANDS = [
|
||||||
("CmdGetInfo", "", ""),
|
("CmdGetInfo", "", ""),
|
||||||
("CmdCreateScanner", "<I", "scan_id"),
|
("CmdCreateScanner", "<I", "scan_id"),
|
||||||
|
@ -256,169 +239,168 @@ class FlicClient(asyncio.Protocol):
|
||||||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
||||||
]
|
]
|
||||||
|
|
||||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
_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_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))
|
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||||
|
|
||||||
def __init__(self, loop, parent=None):
|
def __init__(self, loop,parent=None):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.buffer = b""
|
self.buffer=b""
|
||||||
self.transport = None
|
self.transport=None
|
||||||
self.parent = parent
|
self.parent=parent
|
||||||
self._scanners = {}
|
self._scanners = {}
|
||||||
self._scan_wizards = {}
|
self._scan_wizards = {}
|
||||||
self._connection_channels = {}
|
self._connection_channels = {}
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
self.on_new_verified_button = lambda bd_addr: None
|
self.on_new_verified_button = lambda bd_addr: None
|
||||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: 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_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_info = lambda items: None
|
||||||
self.on_get_button_uuid = lambda addr, uuid: None
|
self.on_get_button_uuid = lambda addr, uuid: None
|
||||||
|
|
||||||
def connection_made(self, transport):
|
def connection_made(self, transport):
|
||||||
self.transport = transport
|
self.transport=transport
|
||||||
if self.parent:
|
if self.parent:
|
||||||
self.parent.register_protocol(self)
|
self.parent.register_protocol(self)
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closes the client. The handle_events() method will return."""
|
"""Closes the client. The handle_events() method will return."""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def add_scanner(self, scanner):
|
def add_scanner(self, scanner):
|
||||||
"""Add a ButtonScanner object.
|
"""Add a ButtonScanner object.
|
||||||
|
|
||||||
The scan will start directly once the scanner is added.
|
The scan will start directly once the scanner is added.
|
||||||
"""
|
"""
|
||||||
if scanner._scan_id in self._scanners:
|
if scanner._scan_id in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scanners[scanner._scan_id] = scanner
|
self._scanners[scanner._scan_id] = scanner
|
||||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def remove_scanner(self, scanner):
|
def remove_scanner(self, scanner):
|
||||||
"""Remove a ButtonScanner object.
|
"""Remove a ButtonScanner object.
|
||||||
|
|
||||||
You will no longer receive advertisement packets.
|
You will no longer receive advertisement packets.
|
||||||
"""
|
"""
|
||||||
if scanner._scan_id not in self._scanners:
|
if scanner._scan_id not in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
del self._scanners[scanner._scan_id]
|
del self._scanners[scanner._scan_id]
|
||||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def add_scan_wizard(self, scan_wizard):
|
def add_scan_wizard(self, scan_wizard):
|
||||||
"""Add a ScanWizard object.
|
"""Add a ScanWizard object.
|
||||||
|
|
||||||
The scan wizard will start directly once the scan wizard is added.
|
The scan wizard will start directly once the scan wizard is added.
|
||||||
"""
|
"""
|
||||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def cancel_scan_wizard(self, scan_wizard):
|
def cancel_scan_wizard(self, scan_wizard):
|
||||||
"""Cancel a ScanWizard.
|
"""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.
|
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 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:
|
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def add_connection_channel(self, channel):
|
def add_connection_channel(self, channel):
|
||||||
"""Adds a connection channel to a specific Flic button.
|
"""Adds a connection channel to a specific Flic button.
|
||||||
|
|
||||||
This will start listening for a specific Flic button's connection and button events.
|
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.
|
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
|
The on_create_connection_channel_response callback property will be called on the
|
||||||
connection channel after this command has been received by the server.
|
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.
|
You may have as many connection channels as you wish for a specific Flic Button.
|
||||||
"""
|
"""
|
||||||
if channel._conn_id in self._connection_channels:
|
if channel._conn_id in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
channel._client = self
|
channel._client = self
|
||||||
|
|
||||||
self._connection_channels[channel._conn_id] = channel
|
self._connection_channels[channel._conn_id] = channel
|
||||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr,
|
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})
|
||||||
"latency_mode": channel._latency_mode,
|
|
||||||
"auto_disconnect_time": channel._auto_disconnect_time})
|
|
||||||
|
|
||||||
def remove_connection_channel(self, channel):
|
def remove_connection_channel(self, channel):
|
||||||
"""Remove a connection channel.
|
"""Remove a connection channel.
|
||||||
|
|
||||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
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.
|
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:
|
if channel._conn_id not in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
||||||
|
|
||||||
def force_disconnect(self, bd_addr):
|
def force_disconnect(self, bd_addr):
|
||||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
"""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.
|
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})
|
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
"""Get info about the current state of the server.
|
"""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 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:
|
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,
|
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).
|
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
|
||||||
"""
|
"""
|
||||||
self._send_command("CmdGetInfo", {})
|
self._send_command("CmdGetInfo", {})
|
||||||
|
|
||||||
def get_button_uuid(self, bd_addr):
|
def get_button_uuid(self, bd_addr):
|
||||||
"""Get button uuid for a verified button.
|
"""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.
|
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.
|
Responses will arrive in the same order as requested.
|
||||||
|
|
||||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
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.
|
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})
|
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
|
|
||||||
def run_on_handle_events_thread(self, callback):
|
def run_on_handle_events_thread(self, callback):
|
||||||
"""Run a function on the thread that handles the events."""
|
"""Run a function on the thread that handles the events."""
|
||||||
if threading.get_ident() == self._handle_event_thread_ident:
|
if threading.get_ident() == self._handle_event_thread_ident:
|
||||||
callback()
|
callback()
|
||||||
else:
|
else:
|
||||||
self.set_timer(0, callback)
|
self.set_timer(0, callback)
|
||||||
|
|
||||||
def _send_command(self, name, items):
|
def _send_command(self, name, items):
|
||||||
|
|
||||||
for key, value in items.items():
|
for key, value in items.items():
|
||||||
if isinstance(value, Enum):
|
if isinstance(value, Enum):
|
||||||
items[key] = value.value
|
items[key] = value.value
|
||||||
|
|
||||||
if "bd_addr" in items:
|
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]
|
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
||||||
bytes = bytearray(3)
|
bytes = bytearray(3)
|
||||||
|
@ -427,85 +409,83 @@ class FlicClient(asyncio.Protocol):
|
||||||
bytes[2] = opcode
|
bytes[2] = opcode
|
||||||
bytes += data_bytes
|
bytes += data_bytes
|
||||||
self.transport.write(bytes)
|
self.transport.write(bytes)
|
||||||
|
|
||||||
def _dispatch_event(self, data):
|
def _dispatch_event(self, data):
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
opcode = data[0]
|
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
|
return
|
||||||
|
|
||||||
event_name = FlicClient._EVENTS[opcode][0]
|
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()
|
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||||
|
|
||||||
# Process some kind of items whose data type is not supported by struct
|
# Process some kind of items whose data type is not supported by struct
|
||||||
if "bd_addr" in items:
|
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:
|
if "name" in items:
|
||||||
items["name"] = items["name"].decode("utf-8")
|
items["name"] = items["name"].decode("utf-8")
|
||||||
|
|
||||||
if event_name == "EvtCreateConnectionChannelResponse":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
items["error"] = CreateConnectionChannelError(items["error"])
|
items["error"] = CreateConnectionChannelError(items["error"])
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||||
|
|
||||||
if event_name.startswith("EvtButton"):
|
if event_name.startswith("EvtButton"):
|
||||||
items["click_type"] = ClickType(items["click_type"])
|
items["click_type"] = ClickType(items["click_type"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
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["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||||
items["bd_addr_of_verified_buttons"] = []
|
items["bd_addr_of_verified_buttons"] = []
|
||||||
|
|
||||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||||
for i in range(items["nb_verified_buttons"]):
|
for i in range(items["nb_verified_buttons"]):
|
||||||
items["bd_addr_of_verified_buttons"].append(
|
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
||||||
FlicClient._bdaddr_bytes_to_string())
|
|
||||||
pos += 6
|
pos += 6
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
items["state"] = BluetoothControllerState(items["state"])
|
items["state"] = BluetoothControllerState(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||||
if items["uuid"] == "00000000000000000000000000000000":
|
if items["uuid"] == "00000000000000000000000000000000":
|
||||||
items["uuid"] = None
|
items["uuid"] = None
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
items["result"] = ScanWizardResult(items["result"])
|
items["result"] = ScanWizardResult(items["result"])
|
||||||
|
|
||||||
# Process event
|
# Process event
|
||||||
if event_name == "EvtAdvertisementPacket":
|
if event_name == "EvtAdvertisementPacket":
|
||||||
scanner = self._scanners.get(items["scan_id"])
|
scanner = self._scanners.get(items["scan_id"])
|
||||||
if scanner is not None:
|
if scanner is not None:
|
||||||
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"],
|
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
|
||||||
items["is_private"], items["already_verified"])
|
|
||||||
|
|
||||||
if event_name == "EvtCreateConnectionChannelResponse":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
if items["error"] != CreateConnectionChannelError.NoError:
|
if items["error"] != CreateConnectionChannelError.NoError:
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_removed(channel, items["removed_reason"])
|
channel.on_removed(channel, items["removed_reason"])
|
||||||
|
|
||||||
if event_name == "EvtButtonUpOrDown":
|
if event_name == "EvtButtonUpOrDown":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
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"])
|
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||||
if event_name == "EvtButtonSingleOrDoubleClick":
|
if event_name == "EvtButtonSingleOrDoubleClick":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"],
|
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||||
items["time_diff"])
|
|
||||||
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"],
|
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||||
items["time_diff"])
|
|
||||||
|
|
||||||
if event_name == "EvtNewVerifiedButton":
|
if event_name == "EvtNewVerifiedButton":
|
||||||
self.on_new_verified_button(items["bd_addr"])
|
self.on_new_verified_button(items["bd_addr"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
self.on_get_info(items)
|
self.on_get_info(items)
|
||||||
|
|
||||||
if event_name == "EvtNoSpaceForNewConnection":
|
if event_name == "EvtNoSpaceForNewConnection":
|
||||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtGotSpaceForNewConnection":
|
if event_name == "EvtGotSpaceForNewConnection":
|
||||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
self.on_bluetooth_controller_state_change(items["state"])
|
self.on_bluetooth_controller_state_change(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
|
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPrivateButton":
|
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_found_private_button(scan_wizard)
|
scan_wizard.on_found_private_button(scan_wizard)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPublicButton":
|
if event_name == "EvtScanWizardFoundPublicButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard._bd_addr = items["bd_addr"]
|
scan_wizard._bd_addr = items["bd_addr"]
|
||||||
scan_wizard._name = items["name"]
|
scan_wizard._name = items["name"]
|
||||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardButtonConnected":
|
if event_name == "EvtScanWizardButtonConnected":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
del 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)
|
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
def data_received(self, data):
|
|
||||||
cdata = self.buffer + data
|
def data_received(self,data):
|
||||||
self.buffer = b""
|
cdata=self.buffer+data
|
||||||
|
self.buffer=b""
|
||||||
while len(cdata):
|
while len(cdata):
|
||||||
packet_len = cdata[0] | (cdata[1] << 8)
|
packet_len = cdata[0] | (cdata[1] << 8)
|
||||||
packet_len += 2
|
packet_len += 2
|
||||||
if len(cdata) >= packet_len:
|
if len(cdata)>= packet_len:
|
||||||
self._dispatch_event(cdata[2:packet_len])
|
self._dispatch_event(cdata[2:packet_len])
|
||||||
cdata = cdata[packet_len:]
|
cdata=cdata[packet_len:]
|
||||||
else:
|
else:
|
||||||
if len(cdata):
|
if len(cdata):
|
||||||
self.buffer = cdata # unlikely to happen but.....
|
self.buffer=cdata #unlikely to happen but.....
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,12 @@ class RemovedReason(Enum):
|
||||||
RemovedByThisClient = 0
|
RemovedByThisClient = 0
|
||||||
ForceDisconnectedByThisClient = 1
|
ForceDisconnectedByThisClient = 1
|
||||||
ForceDisconnectedByOtherClient = 2
|
ForceDisconnectedByOtherClient = 2
|
||||||
|
|
||||||
ButtonIsPrivate = 3
|
ButtonIsPrivate = 3
|
||||||
VerifyTimeout = 4
|
VerifyTimeout = 4
|
||||||
InternetBackendError = 5
|
InternetBackendError = 5
|
||||||
InvalidData = 6
|
InvalidData = 6
|
||||||
|
|
||||||
CouldntLoadDevice = 7
|
CouldntLoadDevice = 7
|
||||||
|
|
||||||
class ClickType(Enum):
|
class ClickType(Enum):
|
||||||
|
@ -81,22 +81,22 @@ class ScanWizardResult(Enum):
|
||||||
|
|
||||||
class ButtonScanner:
|
class ButtonScanner:
|
||||||
"""ButtonScanner class.
|
"""ButtonScanner class.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
scanner = ButtonScanner()
|
scanner = ButtonScanner()
|
||||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
||||||
client.add_scanner(scanner)
|
client.add_scanner(scanner)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_id = next(ButtonScanner._cnt)
|
self._scan_id = next(ButtonScanner._cnt)
|
||||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
||||||
|
|
||||||
class ScanWizard:
|
class ScanWizard:
|
||||||
"""ScanWizard class
|
"""ScanWizard class
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
wizard = ScanWizard()
|
wizard = ScanWizard()
|
||||||
wizard.on_found_private_button = lambda scan_wizard: ...
|
wizard.on_found_private_button = lambda scan_wizard: ...
|
||||||
|
@ -105,9 +105,9 @@ class ScanWizard:
|
||||||
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
||||||
client.add_scan_wizard(wizard)
|
client.add_scan_wizard(wizard)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_wizard_id = next(ScanWizard._cnt)
|
self._scan_wizard_id = next(ScanWizard._cnt)
|
||||||
self._bd_addr = None
|
self._bd_addr = None
|
||||||
|
@ -119,31 +119,31 @@ class ScanWizard:
|
||||||
|
|
||||||
class ButtonConnectionChannel:
|
class ButtonConnectionChannel:
|
||||||
"""ButtonConnectionChannel class.
|
"""ButtonConnectionChannel class.
|
||||||
|
|
||||||
This class represents a connection channel to a Flic button.
|
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).
|
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.
|
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
|
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,
|
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
|
||||||
referencing this object.
|
referencing this object.
|
||||||
|
|
||||||
Available properties and the function parameters are:
|
Available properties and the function parameters are:
|
||||||
on_create_connection_channel_response: channel, error, connection_status
|
on_create_connection_channel_response: channel, error, connection_status
|
||||||
on_removed: channel, removed_reason
|
on_removed: channel, removed_reason
|
||||||
on_connection_status_changed: channel, connection_status, disconnect_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
|
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()
|
_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._conn_id = next(ButtonConnectionChannel._cnt)
|
||||||
self._bd_addr = bd_addr
|
self._bd_addr = bd_addr
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
||||||
self.on_removed = lambda channel, removed_reason: None
|
self.on_removed = lambda channel, removed_reason: None
|
||||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_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_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 = 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
|
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bd_addr(self):
|
def bd_addr(self):
|
||||||
return self._bd_addr
|
return self._bd_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latency_mode(self):
|
def latency_mode(self):
|
||||||
return self._latency_mode
|
return self._latency_mode
|
||||||
|
|
||||||
@latency_mode.setter
|
@latency_mode.setter
|
||||||
def latency_mode(self, latency_mode):
|
def latency_mode(self, latency_mode):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._client._lock:
|
with self._client._lock:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
if not self._client._closed:
|
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
|
@property
|
||||||
def auto_disconnect_time(self):
|
def auto_disconnect_time(self):
|
||||||
return self._auto_disconnect_time
|
return self._auto_disconnect_time
|
||||||
|
|
||||||
@auto_disconnect_time.setter
|
@auto_disconnect_time.setter
|
||||||
def auto_disconnect_time(self, auto_disconnect_time):
|
def auto_disconnect_time(self, auto_disconnect_time):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._client._lock:
|
with self._client._lock:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
|
@ -188,26 +188,26 @@ class ButtonConnectionChannel:
|
||||||
|
|
||||||
class FlicClient:
|
class FlicClient:
|
||||||
"""FlicClient class.
|
"""FlicClient class.
|
||||||
|
|
||||||
When this class is constructed, a socket connection is established.
|
When this class is constructed, a socket connection is established.
|
||||||
You may then send commands to the server and set timers.
|
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.
|
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.
|
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 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.
|
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 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.
|
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):
|
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_new_verified_button: bd_addr
|
||||||
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_bluetooth_controller_state_change: state
|
on_bluetooth_controller_state_change: state
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_EVENTS = [
|
_EVENTS = [
|
||||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
||||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
||||||
|
@ -229,9 +229,9 @@ class FlicClient:
|
||||||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
||||||
]
|
]
|
||||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _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 is None else namedtuple(x[0], x[2]), _EVENTS))
|
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
|
||||||
|
|
||||||
_COMMANDS = [
|
_COMMANDS = [
|
||||||
("CmdGetInfo", "", ""),
|
("CmdGetInfo", "", ""),
|
||||||
("CmdCreateScanner", "<I", "scan_id"),
|
("CmdCreateScanner", "<I", "scan_id"),
|
||||||
|
@ -245,19 +245,17 @@ class FlicClient:
|
||||||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
||||||
]
|
]
|
||||||
|
|
||||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
_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_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))
|
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||||
|
|
||||||
def __init__(self, host, port = 5551):
|
def __init__(self, host, port = 5551):
|
||||||
self._sock = socket.create_connection((host, port), None)
|
self._sock = socket.create_connection((host, port), None)
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
@ -269,113 +267,113 @@ class FlicClient:
|
||||||
self._timers = queue.PriorityQueue()
|
self._timers = queue.PriorityQueue()
|
||||||
self._handle_event_thread_ident = None
|
self._handle_event_thread_ident = None
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
self.on_new_verified_button = lambda bd_addr: None
|
self.on_new_verified_button = lambda bd_addr: None
|
||||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: 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_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
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closes the client. The handle_events() method will return."""
|
"""Closes the client. The handle_events() method will return."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
if threading.get_ident() != self._handle_event_thread_ident:
|
if threading.get_ident() != self._handle_event_thread_ident:
|
||||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||||
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def add_scanner(self, scanner):
|
def add_scanner(self, scanner):
|
||||||
"""Add a ButtonScanner object.
|
"""Add a ButtonScanner object.
|
||||||
|
|
||||||
The scan will start directly once the scanner is added.
|
The scan will start directly once the scanner is added.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scanner._scan_id in self._scanners:
|
if scanner._scan_id in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scanners[scanner._scan_id] = scanner
|
self._scanners[scanner._scan_id] = scanner
|
||||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def remove_scanner(self, scanner):
|
def remove_scanner(self, scanner):
|
||||||
"""Remove a ButtonScanner object.
|
"""Remove a ButtonScanner object.
|
||||||
|
|
||||||
You will no longer receive advertisement packets.
|
You will no longer receive advertisement packets.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scanner._scan_id not in self._scanners:
|
if scanner._scan_id not in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
del self._scanners[scanner._scan_id]
|
del self._scanners[scanner._scan_id]
|
||||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def add_scan_wizard(self, scan_wizard):
|
def add_scan_wizard(self, scan_wizard):
|
||||||
"""Add a ScanWizard object.
|
"""Add a ScanWizard object.
|
||||||
|
|
||||||
The scan wizard will start directly once the scan wizard is added.
|
The scan wizard will start directly once the scan wizard is added.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def cancel_scan_wizard(self, scan_wizard):
|
def cancel_scan_wizard(self, scan_wizard):
|
||||||
"""Cancel a ScanWizard.
|
"""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.
|
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 cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def add_connection_channel(self, channel):
|
def add_connection_channel(self, channel):
|
||||||
"""Adds a connection channel to a specific Flic button.
|
"""Adds a connection channel to a specific Flic button.
|
||||||
|
|
||||||
This will start listening for a specific Flic button's connection and button events.
|
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.
|
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
|
The on_create_connection_channel_response callback property will be called on the
|
||||||
connection channel after this command has been received by the server.
|
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.
|
You may have as many connection channels as you wish for a specific Flic Button.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if channel._conn_id in self._connection_channels:
|
if channel._conn_id in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
channel._client = self
|
channel._client = self
|
||||||
|
|
||||||
self._connection_channels[channel._conn_id] = channel
|
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):
|
def remove_connection_channel(self, channel):
|
||||||
"""Remove a connection channel.
|
"""Remove a connection channel.
|
||||||
|
|
||||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
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.
|
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:
|
with self._lock:
|
||||||
if channel._conn_id not in self._connection_channels:
|
if channel._conn_id not in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
||||||
|
|
||||||
def force_disconnect(self, bd_addr):
|
def force_disconnect(self, bd_addr):
|
||||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
"""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.
|
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})
|
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def get_info(self, callback):
|
def get_info(self, callback):
|
||||||
"""Get info about the current state of the server.
|
"""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 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:
|
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,
|
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._get_info_response_queue.put(callback)
|
||||||
self._send_command("CmdGetInfo", {})
|
self._send_command("CmdGetInfo", {})
|
||||||
|
|
||||||
def get_button_uuid(self, bd_addr, callback):
|
def get_button_uuid(self, bd_addr, callback):
|
||||||
"""Get button uuid for a verified button.
|
"""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.
|
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.
|
Responses will arrive in the same order as requested.
|
||||||
|
|
||||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
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.
|
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._get_button_uuid_queue.put(callback)
|
self._get_button_uuid_queue.put(callback)
|
||||||
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def set_timer(self, timeout_millis, callback):
|
def set_timer(self, timeout_millis, callback):
|
||||||
"""Set a timer
|
"""Set a timer
|
||||||
|
|
||||||
This timer callback will run after the specified timeout_millis on the thread that handles the events.
|
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
|
point_in_time = time.monotonic() + timeout_millis / 1000.0
|
||||||
self._timers.put((point_in_time, callback))
|
self._timers.put((point_in_time, callback))
|
||||||
|
|
||||||
if threading.get_ident() != self._handle_event_thread_ident:
|
if threading.get_ident() != self._handle_event_thread_ident:
|
||||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||||
|
|
||||||
def run_on_handle_events_thread(self, callback):
|
def run_on_handle_events_thread(self, callback):
|
||||||
"""Run a function on the thread that handles the events."""
|
"""Run a function on the thread that handles the events."""
|
||||||
if threading.get_ident() == self._handle_event_thread_ident:
|
if threading.get_ident() == self._handle_event_thread_ident:
|
||||||
callback()
|
callback()
|
||||||
else:
|
else:
|
||||||
self.set_timer(0, callback)
|
self.set_timer(0, callback)
|
||||||
|
|
||||||
def _send_command(self, name, items):
|
def _send_command(self, name, items):
|
||||||
for key, value in items.items():
|
for key, value in items.items():
|
||||||
if isinstance(value, Enum):
|
if isinstance(value, Enum):
|
||||||
items[key] = value.value
|
items[key] = value.value
|
||||||
|
|
||||||
if "bd_addr" in items:
|
if "bd_addr" in items:
|
||||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
||||||
|
|
||||||
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
||||||
bytes = bytearray(3)
|
bytes = bytearray(3)
|
||||||
|
@ -434,83 +432,83 @@ class FlicClient:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if not self._closed:
|
if not self._closed:
|
||||||
self._sock.sendall(bytes)
|
self._sock.sendall(bytes)
|
||||||
|
|
||||||
def _dispatch_event(self, data):
|
def _dispatch_event(self, data):
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
opcode = data[0]
|
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
|
return
|
||||||
|
|
||||||
event_name = FlicClient._EVENTS[opcode][0]
|
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()
|
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||||
|
|
||||||
# Process some kind of items whose data type is not supported by struct
|
# Process some kind of items whose data type is not supported by struct
|
||||||
if "bd_addr" in items:
|
if "bd_addr" in items:
|
||||||
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
||||||
|
|
||||||
if "name" in items:
|
if "name" in items:
|
||||||
items["name"] = items["name"].decode("utf-8")
|
items["name"] = items["name"].decode("utf-8")
|
||||||
|
|
||||||
if event_name == "EvtCreateConnectionChannelResponse":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
items["error"] = CreateConnectionChannelError(items["error"])
|
items["error"] = CreateConnectionChannelError(items["error"])
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||||
|
|
||||||
if event_name.startswith("EvtButton"):
|
if event_name.startswith("EvtButton"):
|
||||||
items["click_type"] = ClickType(items["click_type"])
|
items["click_type"] = ClickType(items["click_type"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
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"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
|
||||||
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||||
items["bd_addr_of_verified_buttons"] = []
|
items["bd_addr_of_verified_buttons"] = []
|
||||||
|
|
||||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||||
for i in range(items["nb_verified_buttons"]):
|
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]))
|
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
||||||
pos += 6
|
pos += 6
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
items["state"] = BluetoothControllerState(items["state"])
|
items["state"] = BluetoothControllerState(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||||
if items["uuid"] == "00000000000000000000000000000000":
|
if items["uuid"] == "00000000000000000000000000000000":
|
||||||
items["uuid"] = None
|
items["uuid"] = None
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
items["result"] = ScanWizardResult(items["result"])
|
items["result"] = ScanWizardResult(items["result"])
|
||||||
|
|
||||||
# Process event
|
# Process event
|
||||||
if event_name == "EvtAdvertisementPacket":
|
if event_name == "EvtAdvertisementPacket":
|
||||||
scanner = self._scanners.get(items["scan_id"])
|
scanner = self._scanners.get(items["scan_id"])
|
||||||
if scanner is not None:
|
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":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
if items["error"] != CreateConnectionChannelError.NoError:
|
if items["error"] != CreateConnectionChannelError.NoError:
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_removed(channel, items["removed_reason"])
|
channel.on_removed(channel, items["removed_reason"])
|
||||||
|
|
||||||
if event_name == "EvtButtonUpOrDown":
|
if event_name == "EvtButtonUpOrDown":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
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":
|
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
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":
|
if event_name == "EvtNewVerifiedButton":
|
||||||
self.on_new_verified_button(items["bd_addr"])
|
self.on_new_verified_button(items["bd_addr"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
self._get_info_response_queue.get()(items)
|
self._get_info_response_queue.get()(items)
|
||||||
|
|
||||||
if event_name == "EvtNoSpaceForNewConnection":
|
if event_name == "EvtNoSpaceForNewConnection":
|
||||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtGotSpaceForNewConnection":
|
if event_name == "EvtGotSpaceForNewConnection":
|
||||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
self.on_bluetooth_controller_state_change(items["state"])
|
self.on_bluetooth_controller_state_change(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
|
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPrivateButton":
|
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_found_private_button(scan_wizard)
|
scan_wizard.on_found_private_button(scan_wizard)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPublicButton":
|
if event_name == "EvtScanWizardFoundPublicButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard._bd_addr = items["bd_addr"]
|
scan_wizard._bd_addr = items["bd_addr"]
|
||||||
scan_wizard._name = items["name"]
|
scan_wizard._name = items["name"]
|
||||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardButtonConnected":
|
if event_name == "EvtScanWizardButtonConnected":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
del 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)
|
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
def _handle_one_event(self):
|
def _handle_one_event(self):
|
||||||
if len(self._timers.queue) > 0:
|
if len(self._timers.queue) > 0:
|
||||||
current_timer = self._timers.queue[0]
|
current_timer = self._timers.queue[0]
|
||||||
|
@ -570,10 +568,10 @@ class FlicClient:
|
||||||
return True
|
return True
|
||||||
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
|
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
len_arr = bytearray(2)
|
len_arr = bytearray(2)
|
||||||
view = memoryview(len_arr)
|
view = memoryview(len_arr)
|
||||||
|
|
||||||
toread = 2
|
toread = 2
|
||||||
while toread > 0:
|
while toread > 0:
|
||||||
nbytes = self._sock.recv_into(view, toread)
|
nbytes = self._sock.recv_into(view, toread)
|
||||||
|
@ -581,7 +579,7 @@ class FlicClient:
|
||||||
return False
|
return False
|
||||||
view = view[nbytes:]
|
view = view[nbytes:]
|
||||||
toread -= nbytes
|
toread -= nbytes
|
||||||
|
|
||||||
packet_len = len_arr[0] | (len_arr[1] << 8)
|
packet_len = len_arr[0] | (len_arr[1] << 8)
|
||||||
data = bytearray(packet_len)
|
data = bytearray(packet_len)
|
||||||
view = memoryview(data)
|
view = memoryview(data)
|
||||||
|
@ -592,13 +590,13 @@ class FlicClient:
|
||||||
return False
|
return False
|
||||||
view = view[nbytes:]
|
view = view[nbytes:]
|
||||||
toread -= nbytes
|
toread -= nbytes
|
||||||
|
|
||||||
self._dispatch_event(data)
|
self._dispatch_event(data)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def handle_events(self):
|
def handle_events(self):
|
||||||
"""Start the main loop for this client.
|
"""Start the main loop for this client.
|
||||||
|
|
||||||
This method will not return until the socket has been closed.
|
This method will not return until the socket has been closed.
|
||||||
Once it has returned, any use of this FlicClient is illegal.
|
Once it has returned, any use of this FlicClient is illegal.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -34,7 +34,7 @@ class CameraPiBackend(Backend):
|
||||||
return self.value == other
|
return self.value == other
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
# 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',
|
redis_queue='platypush/camera/pi',
|
||||||
start_recording_on_startup=True,
|
start_recording_on_startup=True,
|
||||||
framerate=24, hflip=False, vflip=False,
|
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
|
:param listen_port: Port where the camera process will provide the video output while recording
|
||||||
:type listen_port: int
|
:type listen_port: int
|
||||||
|
|
||||||
:param bind_address: Bind address (default: 0.0.0.0).
|
|
||||||
:type bind_address: str
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.bind_address = bind_address
|
|
||||||
self.listen_port = listen_port
|
self.listen_port = listen_port
|
||||||
self.server_socket = socket.socket()
|
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)
|
self.server_socket.listen(0)
|
||||||
|
|
||||||
import picamera
|
import picamera
|
||||||
|
@ -138,13 +134,13 @@ class CameraPiBackend(Backend):
|
||||||
self.logger.info('Client closed connection')
|
self.logger.info('Client closed connection')
|
||||||
try:
|
try:
|
||||||
self.stop_recording()
|
self.stop_recording()
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.warning('Could not stop recording: {}'.format(str(e)))
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connection.close()
|
connection.close()
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.warning('Could not close connection: {}'.format(str(e)))
|
pass
|
||||||
|
|
||||||
self.send_camera_action(self.CameraAction.START_RECORDING)
|
self.send_camera_action(self.CameraAction.START_RECORDING)
|
||||||
|
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
from typing import Iterable, Dict, Union, Any
|
|
||||||
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from .entities.handlers import EventHandler
|
|
||||||
from .entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
|
|
||||||
|
|
||||||
|
|
||||||
class FileMonitorBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend monitors changes to local files and directories using the Watchdog API.
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
* :class:`platypush.message.event.file.FileSystemCreateEvent` if a resource is created.
|
|
||||||
* :class:`platypush.message.event.file.FileSystemDeleteEvent` if a resource is removed.
|
|
||||||
* :class:`platypush.message.event.file.FileSystemModifyEvent` if a resource is modified.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **watchdog** (``pip install watchdog``)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
class EventHandlerFactory:
|
|
||||||
"""
|
|
||||||
Create a file system event handler from a string, dictionary or ``MonitoredResource`` resource.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_resource(resource: Union[str, Dict[str, Any], MonitoredResource]) -> EventHandler:
|
|
||||||
if isinstance(resource, str):
|
|
||||||
resource = MonitoredResource(resource)
|
|
||||||
elif isinstance(resource, dict):
|
|
||||||
if 'patterns' in resource or 'ignore_patterns' in resource:
|
|
||||||
resource = MonitoredPattern(**resource)
|
|
||||||
elif 'regexes' in resource or 'ignore_regexes' in resource:
|
|
||||||
resource = MonitoredRegex(**resource)
|
|
||||||
else:
|
|
||||||
resource = MonitoredResource(**resource)
|
|
||||||
|
|
||||||
return EventHandler.from_resource(resource)
|
|
||||||
|
|
||||||
def __init__(self, paths: Iterable[Union[str, Dict[str, Any], MonitoredResource]], **kwargs):
|
|
||||||
"""
|
|
||||||
:param paths: List of paths to monitor. Paths can either be expressed in any of the following ways:
|
|
||||||
|
|
||||||
- Simple strings. In this case, paths will be interpreted as absolute references to a file or a directory
|
|
||||||
to monitor. Example:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Monitor changes on the /tmp folder
|
|
||||||
- /tmp
|
|
||||||
# Monitor changes on /etc/passwd
|
|
||||||
- /etc/passwd
|
|
||||||
|
|
||||||
- Path with monitoring properties expressed as a key-value object. Example showing the supported attributes:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Monitor changes on the /tmp folder and its subfolders
|
|
||||||
- path: /tmp
|
|
||||||
recursive: True
|
|
||||||
|
|
||||||
- Path with pattern-based search criteria for the files to monitor and exclude. Example:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Recursively monitor changes on the ~/my-project folder that include all
|
|
||||||
# *.py files, excluding those whose name starts with tmp_ and
|
|
||||||
# all the files contained in the __pycache__ folders
|
|
||||||
- path: ~/my-project
|
|
||||||
recursive: True
|
|
||||||
patterns:
|
|
||||||
- "*.py"
|
|
||||||
ignore_patterns:
|
|
||||||
- "tmp_*"
|
|
||||||
ignore_directories:
|
|
||||||
- "__pycache__"
|
|
||||||
|
|
||||||
- Path with regex-based search criteria for the files to monitor and exclude. Example:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Recursively monitor changes on the ~/my-images folder that include all
|
|
||||||
# the files matching a JPEG extension in case-insensitive mode,
|
|
||||||
# excluding those whose name starts with tmp_ and
|
|
||||||
# all the files contained in the __MACOSX folders
|
|
||||||
- path: ~/my-images
|
|
||||||
recursive: True
|
|
||||||
case_sensitive: False
|
|
||||||
regexes:
|
|
||||||
- '.*\\.jpe?g$'
|
|
||||||
ignore_patterns:
|
|
||||||
- '^tmp_.*'
|
|
||||||
ignore_directories:
|
|
||||||
- '__MACOSX'
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._observer = Observer()
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
handler = self.EventHandlerFactory.from_resource(path)
|
|
||||||
self._observer.schedule(handler, handler.resource.path, recursive=handler.resource.recursive)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
self.logger.info('Initializing file monitor backend')
|
|
||||||
self._observer.start()
|
|
||||||
self.wait_stop()
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
self.logger.info('Stopping file monitor backend')
|
|
||||||
self._observer.stop()
|
|
||||||
self._observer.join()
|
|
||||||
self.logger.info('Stopped file monitor backend')
|
|
|
@ -1,61 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler, RegexMatchingEventHandler
|
|
||||||
|
|
||||||
from platypush.backend.file.monitor.entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
|
|
||||||
from platypush.context import get_bus
|
|
||||||
from platypush.message.event.file import FileSystemModifyEvent, FileSystemCreateEvent, FileSystemDeleteEvent
|
|
||||||
|
|
||||||
|
|
||||||
class EventHandler(FileSystemEventHandler):
|
|
||||||
"""
|
|
||||||
Base class for Watchdog event handlers.
|
|
||||||
"""
|
|
||||||
def __init__(self, resource: MonitoredResource, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
resource.path = os.path.expanduser(resource.path)
|
|
||||||
self.resource = resource
|
|
||||||
|
|
||||||
def on_created(self, event):
|
|
||||||
get_bus().post(FileSystemCreateEvent(path=event.src_path, is_directory=event.is_directory))
|
|
||||||
|
|
||||||
def on_deleted(self, event):
|
|
||||||
get_bus().post(FileSystemDeleteEvent(path=event.src_path, is_directory=event.is_directory))
|
|
||||||
|
|
||||||
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):
|
|
||||||
return PatternEventHandler(resource)
|
|
||||||
if isinstance(resource, MonitoredRegex):
|
|
||||||
return RegexEventHandler(resource)
|
|
||||||
return cls(resource)
|
|
||||||
|
|
||||||
|
|
||||||
class PatternEventHandler(EventHandler, PatternMatchingEventHandler):
|
|
||||||
"""
|
|
||||||
Event handler for file patterns.
|
|
||||||
"""
|
|
||||||
def __init__(self, resource: MonitoredPattern):
|
|
||||||
super().__init__(resource=resource,
|
|
||||||
patterns=resource.patterns,
|
|
||||||
ignore_patterns=resource.ignore_patterns,
|
|
||||||
ignore_directories=resource.ignore_directories,
|
|
||||||
case_sensitive=resource.case_sensitive)
|
|
||||||
|
|
||||||
|
|
||||||
class RegexEventHandler(EventHandler, RegexMatchingEventHandler):
|
|
||||||
"""
|
|
||||||
Event handler for regex-based file patterns.
|
|
||||||
"""
|
|
||||||
def __init__(self, resource: MonitoredRegex):
|
|
||||||
super().__init__(resource=resource,
|
|
||||||
regexes=resource.regexes,
|
|
||||||
ignore_regexes=resource.ignore_regexes,
|
|
||||||
ignore_directories=resource.ignore_directories,
|
|
||||||
case_sensitive=resource.case_sensitive)
|
|
|
@ -1,24 +0,0 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MonitoredResource:
|
|
||||||
path: str
|
|
||||||
recursive: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MonitoredPattern(MonitoredResource):
|
|
||||||
patterns: Optional[List[str]] = None
|
|
||||||
ignore_patterns: Optional[List[str]] = None
|
|
||||||
ignore_directories: Optional[List[str]] = None
|
|
||||||
case_sensitive: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MonitoredRegex(MonitoredResource):
|
|
||||||
regexes: Optional[List[str]] = None
|
|
||||||
ignore_regexes: Optional[List[str]] = None
|
|
||||||
ignore_directories: Optional[List[str]] = None
|
|
||||||
case_sensitive: bool = True
|
|
|
@ -51,10 +51,11 @@ class GooglePubsubBackend(Backend):
|
||||||
def _message_callback(self, topic):
|
def _message_callback(self, topic):
|
||||||
def callback(msg):
|
def callback(msg):
|
||||||
data = msg.data.decode()
|
data = msg.data.decode()
|
||||||
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e)))
|
pass
|
||||||
|
|
||||||
msg.ack()
|
msg.ack()
|
||||||
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
|
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
|
||||||
|
|
|
@ -31,8 +31,8 @@ def _hook(hook_name):
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
event_args['data'] = json.loads(event_args['data'])
|
event_args['data'] = json.loads(event_args['data'])
|
||||||
except Exception as e:
|
except:
|
||||||
logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e)))
|
pass
|
||||||
|
|
||||||
event = WebhookEvent(**event_args)
|
event = WebhookEvent(**event_args)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ def login():
|
||||||
if session_token:
|
if session_token:
|
||||||
user, session = user_manager.authenticate_user_session(session_token)
|
user, session = user_manager.authenticate_user_session(session_token)
|
||||||
if user:
|
if user:
|
||||||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
return redirect(redirect_page, 302)
|
||||||
|
|
||||||
if request.form:
|
if request.form:
|
||||||
username = request.form.get('username')
|
username = request.form.get('username')
|
||||||
|
@ -44,7 +44,7 @@ def login():
|
||||||
expires_at=expires)
|
expires_at=expires)
|
||||||
|
|
||||||
if session:
|
if session:
|
||||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
redirect_target = redirect(redirect_page, 302)
|
||||||
response = make_response(redirect_target)
|
response = make_response(redirect_target)
|
||||||
response.set_cookie('session_token', session.session_token, expires=expires)
|
response.set_cookie('session_token', session.session_token, expires=expires)
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -25,7 +25,7 @@ def logout():
|
||||||
if not user:
|
if not user:
|
||||||
return abort(403, 'Invalid session token')
|
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 = make_response(redirect_target)
|
||||||
response.set_cookie('session_token', '', expires=0)
|
response.set_cookie('session_token', '', expires=0)
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -45,12 +45,14 @@ def get_args(kwargs):
|
||||||
if k == 'resolution':
|
if k == 'resolution':
|
||||||
v = json.loads('[{}]'.format(v))
|
v = json.loads('[{}]'.format(v))
|
||||||
else:
|
else:
|
||||||
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
v = int(v)
|
v = int(v)
|
||||||
except (ValueError, TypeError):
|
except:
|
||||||
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
v = float(v)
|
v = float(v)
|
||||||
except (ValueError, TypeError):
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
kwargs[k] = v
|
kwargs[k] = v
|
||||||
|
|
|
@ -32,8 +32,8 @@ def add_media():
|
||||||
args = {}
|
args = {}
|
||||||
try:
|
try:
|
||||||
args = json.loads(request.data.decode('utf-8'))
|
args = json.loads(request.data.decode('utf-8'))
|
||||||
except Exception as e:
|
except:
|
||||||
abort(400, 'Invalid JSON request: {}'.format(str(e)))
|
abort(400, 'Invalid JSON request')
|
||||||
|
|
||||||
source = args.get('source')
|
source = args.get('source')
|
||||||
if not source:
|
if not source:
|
||||||
|
|
|
@ -40,7 +40,7 @@ def audio_feed(device, fifo, sample_rate, blocksize, latency, channels):
|
||||||
channels=channels)
|
channels=channels)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(fifo, 'rb') as f: # lgtm [py/path-injection]
|
with open(fifo, 'rb') as f:
|
||||||
send_header = True
|
send_header = True
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -31,10 +31,10 @@ def register():
|
||||||
if session_token:
|
if session_token:
|
||||||
user, session = user_manager.authenticate_user_session(session_token)
|
user, session = user_manager.authenticate_user_session(session_token)
|
||||||
if user:
|
if user:
|
||||||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
return redirect(redirect_page, 302)
|
||||||
|
|
||||||
if user_manager.get_user_count() > 0:
|
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:
|
if request.form:
|
||||||
username = request.form.get('username')
|
username = request.form.get('username')
|
||||||
|
@ -49,7 +49,7 @@ def register():
|
||||||
if not remember else None)
|
if not remember else None)
|
||||||
|
|
||||||
if session:
|
if session:
|
||||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
redirect_target = redirect(redirect_page, 302)
|
||||||
response = make_response(redirect_target)
|
response = make_response(redirect_target)
|
||||||
response.set_cookie('session_token', session.session_token)
|
response.set_cookie('session_token', session.session_token)
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -23,7 +23,7 @@ _logger = None
|
||||||
def bus():
|
def bus():
|
||||||
global _bus
|
global _bus
|
||||||
if _bus is None:
|
if _bus is None:
|
||||||
_bus = RedisBus(redis_queue=current_app.config.get('redis_queue'))
|
_bus = RedisBus(redis_queue=current_app.config['redis_queue'])
|
||||||
return _bus
|
return _bus
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,8 +123,7 @@ def _authenticate_token():
|
||||||
try:
|
try:
|
||||||
user_manager.validate_jwt_token(user_token)
|
user_manager.validate_jwt_token(user_token)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except:
|
||||||
logger().debug(str(e))
|
|
||||||
return token and user_token == token
|
return token and user_token == token
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -78,11 +78,11 @@ class HttpRequest(object):
|
||||||
|
|
||||||
def get_new_items(self, response):
|
def get_new_items(self, response):
|
||||||
""" Gets new items out of a 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):
|
def __iter__(self):
|
||||||
for (key, value) in self.request_args.items():
|
for (key, value) in self.request_args.items():
|
||||||
yield key, value
|
yield (key, value)
|
||||||
|
|
||||||
|
|
||||||
class JsonHttpRequest(HttpRequest):
|
class JsonHttpRequest(HttpRequest):
|
||||||
|
@ -96,7 +96,7 @@ class JsonHttpRequest(HttpRequest):
|
||||||
new_entries = []
|
new_entries = []
|
||||||
|
|
||||||
if self.path:
|
if self.path:
|
||||||
m = re.match(r'\${\s*(.*)\s*}', self.path)
|
m = re.match('\$\{\s*(.*)\s*\}', self.path)
|
||||||
response = eval(m.group(1))
|
response = eval(m.group(1))
|
||||||
|
|
||||||
for entry in response:
|
for entry in response:
|
||||||
|
|
|
@ -238,15 +238,15 @@ class RssUpdates(HttpRequest):
|
||||||
with open(digest_filename, 'w', encoding='utf-8') as f:
|
with open(digest_filename, 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
elif self.digest_format == 'pdf':
|
elif self.digest_format == 'pdf':
|
||||||
from weasyprint import HTML, CSS
|
import weasyprint
|
||||||
from weasyprint.fonts import FontConfiguration
|
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()
|
font_config = FontConfiguration()
|
||||||
css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'),
|
css = [weasyprint.CSS('https://fonts.googleapis.com/css?family=Merriweather'),
|
||||||
CSS(string=body_style, font_config=font_config)]
|
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:
|
else:
|
||||||
raise RuntimeError('Unsupported format: {}. Supported formats: ' +
|
raise RuntimeError('Unsupported format: {}. Supported formats: ' +
|
||||||
'html or pdf'.format(self.digest_format))
|
'html or pdf'.format(self.digest_format))
|
||||||
|
|
|
@ -50,13 +50,15 @@ class HttpUtils(object):
|
||||||
for name, resource_path in resource_dirs.items():
|
for name, resource_path in resource_dirs.items():
|
||||||
resource_path = os.path.abspath(os.path.expanduser(resource_path))
|
resource_path = os.path.abspath(os.path.expanduser(resource_path))
|
||||||
if directory.startswith(resource_path):
|
if directory.startswith(resource_path):
|
||||||
|
subdir = re.sub('^{}(.*)$'.format(resource_path),
|
||||||
|
'\\1', directory)
|
||||||
uri = '/resources/' + name
|
uri = '/resources/' + name
|
||||||
break
|
break
|
||||||
|
|
||||||
if not uri:
|
if not uri:
|
||||||
raise RuntimeError(('Directory {} not found among the available ' +
|
raise RuntimeError(('Directory {} not found among the available ' +
|
||||||
'static resources on the webserver').format(
|
'static resources on the webserver').format(
|
||||||
directory))
|
directory))
|
||||||
|
|
||||||
results = [
|
results = [
|
||||||
re.sub('^{}(.*)$'.format(resource_path), uri + '\\1', path)
|
re.sub('^{}(.*)$'.format(resource_path), uri + '\\1', path)
|
||||||
|
@ -90,11 +92,10 @@ class HttpUtils(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def plugin_name_to_tag(cls, module_name):
|
def plugin_name_to_tag(cls, module_name):
|
||||||
return module_name.replace('.', '-')
|
return module_name.replace('.','-')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_templates_in_dir(cls, directory):
|
def find_templates_in_dir(cls, directory):
|
||||||
# noinspection PyTypeChecker
|
|
||||||
return [
|
return [
|
||||||
os.path.join(directory, file)
|
os.path.join(directory, file)
|
||||||
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))
|
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))
|
||||||
|
|
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 4.2 KiB |
Before ![]() (image error) Size: 1.6 KiB |
Before ![]() (image error) Size: 4.2 KiB |
Before ![]() (image error) Size: 1.5 KiB |
Before ![]() (image error) Size: 3.8 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1.6 KiB |
Before ![]() (image error) Size: 1.6 KiB |
Before ![]() (image error) Size: 1.7 KiB |
Before ![]() (image error) Size: 1.7 KiB |
Before ![]() (image error) Size: 1.9 KiB |
Before ![]() (image error) Size: 1.9 KiB |
Before ![]() (image error) Size: 1.2 KiB |
Before ![]() (image error) Size: 1.2 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1.2 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 4.2 KiB |
Before ![]() (image error) Size: 1.6 KiB |
Before ![]() (image error) Size: 4.2 KiB |
Before ![]() (image error) Size: 1.5 KiB |
Before ![]() (image error) Size: 3.8 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1.6 KiB |
Before ![]() (image error) Size: 1.6 KiB |
Before ![]() (image error) Size: 1.7 KiB |
Before ![]() (image error) Size: 1.7 KiB |
Before ![]() (image error) Size: 1.9 KiB |
Before ![]() (image error) Size: 1.9 KiB |
Before ![]() (image error) Size: 1.2 KiB |
Before ![]() (image error) Size: 1.2 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1 KiB |
Before ![]() (image error) Size: 1.2 KiB |
Before ![]() (image error) Size: 1.3 KiB |
Before ![]() (image error) Size: 2.4 KiB |
Before ![]() (image error) Size: 1.8 KiB |