Compare commits

...

64 Commits

Author SHA1 Message Date
Fabio Manganiello db8ea33b68 Removed deprecated `setup.py web_build` command from platydock [see #195] 2021-07-05 20:07:55 +02:00
Fabio Manganiello 03631bcebc Fixed import error in Adafruit.IO 2021-07-04 23:49:18 +02:00
Fabio Manganiello ade3a7c2cf Added plugin_name to weather.buienradar events 2021-07-04 18:04:51 +02:00
Fabio Manganiello 1f6c7aae60 get_redis() should be a general utility method 2021-06-26 11:14:26 +02:00
Fabio Manganiello a6c7d64511 Removed audio_format option from Spotify Connect backend (not supported by all versions of Librespot) 2021-06-25 23:20:21 +02:00
Fabio Manganiello af7977bcf7 Added music.spotify.connect backend 2021-06-25 22:47:40 +02:00
Fabio Manganiello 0762004838 Bump version: 0.21.0 → 0.21.1 2021-06-22 23:40:53 +02:00
Fabio Manganiello c8bfbae4f0 Prevented an infinite recursion error on the Pushbullet on_error() handler in case close() failed 2021-05-20 02:06:43 +02:00
Fabio Manganiello d35a9729a4 More robust reconnection logic for Pushbullet backend 2021-05-19 18:44:01 +02:00
Fabio Manganiello a39452124d Refactored PCA9685 backend 2021-05-17 15:32:43 +02:00
Fabio Manganiello fc1d9ad3e6 Added joystick.linux backend 2021-05-17 14:52:08 +02:00
Fabio Manganiello 7ee869ce42 More robust logic for smooth transients on PCA9685 2021-05-16 18:14:02 +02:00
Fabio Manganiello df36a9f811 s/execute/write/ 2021-05-16 17:53:22 +02:00
Fabio Manganiello abf793e703 Added get_channels() method to PCA9685 driver 2021-05-16 17:51:51 +02:00
Fabio Manganiello 132c659d3c Reset self._pca to None on deinit() 2021-05-16 17:42:05 +02:00
Fabio Manganiello acc4f1c0e3 Added PCA9685 PWM driver plugin 2021-05-16 17:29:03 +02:00
Fabio Manganiello d7d5bcdd0c Wait until the joystick device is readable after it appears to prevent race conditions where jstest fails with temporary "permission denied" errors 2021-05-16 00:26:28 +02:00
Fabio Manganiello def8c0dd76 The joystick backend should properly jstest even when the jstest executable fails 2021-05-16 00:16:19 +02:00
Fabio Manganiello 6cc28a3c3b More robust logic in case of joystick device lost while the backend is running 2021-05-16 00:06:20 +02:00
Fabio Manganiello 93c3327bcd Map name typo fix 2021-05-15 23:53:24 +02:00
Fabio Manganiello 85d975edc6 Logic typo 2021-05-15 23:50:23 +02:00
Fabio Manganiello d767cafafe `joystick.jstest` should actually run the parent `run` method but not extend `JoystickBackend` 2021-05-15 23:48:17 +02:00
Fabio Manganiello cee8f9f8e0 `joystick.jstest` should not execute the parent `run` method 2021-05-15 23:43:37 +02:00
Fabio Manganiello b2e2ae9538 Proper initialization for device attribute in parent joystick backend class 2021-05-15 23:34:41 +02:00
Fabio Manganiello f296f4b161 Added generic `joystick.jstest` backend 2021-05-15 23:28:24 +02:00
Fabio Manganiello 9eab526e47 Specify propertyKey on set_value() if exposed/required by the value payload [see #188] 2021-05-13 22:38:04 +02:00
Fabio Manganiello 8f6404d0b1 Revert "Support for custom timeout on MQTT message publish" (already implemented in the current logic) 2021-05-13 21:49:24 +02:00
Fabio Manganiello eac26b9b22 CHANGELOG edit 2021-05-13 21:37:45 +02:00
Fabio Manganiello b42c491390 Support for custom timeout on MQTT message publish 2021-05-13 21:33:08 +02:00
Fabio Manganiello c7fb97cdc7 Updated CHANGELOG 2021-05-10 21:23:15 +02:00
Fabio Manganiello 18e99c6f12 Added new Google Fit scopes for sleep and heart rate read 2021-05-10 21:21:03 +02:00
Fabio Manganiello 664ce4050d Added Switchbot plugin 2021-05-10 18:43:00 +02:00
Fabio Manganiello 69583d2e15 Added support for Marshmallow schemas in responses 2021-05-10 18:42:13 +02:00
Fabio Manganiello 2f840200be Updated UI files 2021-05-10 18:40:35 +02:00
Fabio Manganiello 46aef7c8b5 autodoc fixes 2021-05-08 21:38:32 +02:00
Fabio Manganiello 5ca15937e3 Bump version: 0.20.10 → 0.21.0 2021-05-06 23:21:11 +02:00
Fabio Manganiello ce882381c0 Fixes to torrent search + SASS library migration
- Support for custom PopcornTime API mirror/base URL.

- Full support for TV series search.

- 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.
2021-05-06 23:18:47 +02:00
Fabio Manganiello 99b35b292f
Merge pull request #180 from BlackLight/snyk-upgrade-8faf0370e1fe9c73606043b43c1f95fc
[Snyk] Upgrade core-js from 3.7.0 to 3.10.1
2021-05-04 01:10:12 +02:00
snyk-bot 174439a8ed
fix: upgrade core-js from 3.7.0 to 3.10.1
Snyk has created this PR to upgrade core-js from 3.7.0 to 3.10.1.

See this package in npm:
https://www.npmjs.com/package/core-js

See this project in Snyk:
https://app.snyk.io/org/blacklight/project/96bfd125-5816-4d9e-83c6-94d1569ab0f1?utm_source=github&utm_medium=upgrade-pr
2021-04-28 22:35:45 +00:00
Fabio Manganiello 3a18e9faf4 Upgraded npm dependencies 2021-04-28 23:55:01 +02:00
Fabio Manganiello f8d76fe4eb Bumped chalk/ssri versions 2021-04-28 23:43:45 +02:00
Fabio Manganiello 9fa3385766 Bump version: 0.20.9 → 0.20.10 2021-04-28 23:06:14 +02:00
Fabio Manganiello 4fe5322600 Explicitly case propertyKey to str 2021-04-22 23:23:41 +02:00
Fabio Manganiello 2224681e3c Removed OZW type references altogether to prevent import errors 2021-04-18 02:27:33 +02:00
Fabio Manganiello 68c44c0c3c OZW objects should be imported inside of the `ZwavePlugin` class to prevent `ImportError` on other Z-Wave plugins that don't depend on OZW 2021-04-18 02:19:53 +02:00
Fabio Manganiello 02a22d4a88 The zwave and zwave.mqtt plugins should extend a common abstract class instead of having a zwave.mqtt -> zwave functional dependency that introduces the PyOWZ dependency into zwave.mqtt 2021-04-16 20:54:07 +02:00
Fabio Manganiello b9bc4b5fe0 Bump version: 0.20.8 → 0.20.9 2021-04-12 02:48:30 +02:00
Fabio Manganiello c006c4b368 Added zwave.mqtt plugin and backend [closes #186] 2021-04-12 02:45:59 +02:00
Fabio Manganiello 75e1f35523
Merge pull request #173 from BlackLight/snyk-upgrade-13988d07be83d12370897cdf2a722b2a
[Snyk] Upgrade @fortawesome/fontawesome-free from 5.15.1 to 5.15.3
2021-04-08 21:18:25 +02:00
Fabio Manganiello 9e08b731a5
Merge pull request #172 from BlackLight/snyk-upgrade-2c28692e689ab7a78e388455f598523f
[Snyk] Upgrade vue-router from 4.0.0-rc.3 to 4.0.5
2021-04-08 21:18:05 +02:00
snyk-bot edfa5ed16f
fix: upgrade @fortawesome/fontawesome-free from 5.15.1 to 5.15.3
Snyk has created this PR to upgrade @fortawesome/fontawesome-free from 5.15.1 to 5.15.3.

See this package in npm:
https://www.npmjs.com/package/@fortawesome/fontawesome-free

See this project in Snyk:
https://app.snyk.io/org/blacklight/project/96bfd125-5816-4d9e-83c6-94d1569ab0f1?utm_source=github&utm_medium=upgrade-pr
2021-04-06 22:36:19 +00:00
snyk-bot f2628f4f2c
fix: upgrade vue-router from 4.0.0-rc.3 to 4.0.5
Snyk has created this PR to upgrade vue-router from 4.0.0-rc.3 to 4.0.5.

See this package in npm:
https://www.npmjs.com/package/vue-router

See this project in Snyk:
https://app.snyk.io/org/blacklight/project/96bfd125-5816-4d9e-83c6-94d1569ab0f1?utm_source=github&utm_medium=upgrade-pr
2021-04-06 22:36:16 +00:00
Fabio Manganiello f1faa1141e More LINT fixes 2021-04-06 21:10:48 +02:00
Fabio Manganiello 2a78f81a7b Major LINT fixes 2021-04-05 00:58:44 +02:00
Fabio Manganiello 86761e7088 Bump version: 0.20.7 → 0.20.8 2021-04-04 00:15:11 +02:00
Fabio Manganiello 89beab4767 Added controls to music dashboard widgets 2021-04-03 21:16:22 +02:00
Fabio Manganiello 8db8f3e6c4 Unparseable context variables should be logged on debug, not warning, level. 2021-04-03 16:12:51 +02:00
Fabio Manganiello ee0685363e Fixed regression on UI event handler callbacks 2021-04-02 19:48:13 +02:00
Fabio Manganiello 33276bf697 Updated CHANGELOG.md 2021-04-02 19:30:57 +02:00
Fabio Manganiello 99831bf0c7 Fix compatibility with all versions of websocket-client, regardless of the list of arguments required by the callbacks (either ws as a first argument or not) 2021-04-02 19:27:25 +02:00
Fabio Manganiello 641d9c0d41 Updated CHANGELOG.md 2021-04-01 22:46:45 +02:00
Fabio Manganiello 95625a401d Skip MQTT message if it has no content [closes #184] 2021-04-01 21:33:44 +02:00
Fabio Manganiello a147a4d37a Added <Camera> dashboard widget 2021-03-29 21:14:32 +02:00
Fabio Manganiello 177c697f83 Added support for custom dashboard components [see #129] 2021-03-28 17:34:11 +02:00
455 changed files with 25050 additions and 5494 deletions

View File

@ -3,6 +3,94 @@
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 ## [0.20.7] - 2021-03-26
### Fixed ### Fixed

View File

@ -7,6 +7,8 @@ Platypush
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt) [![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/) [![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md) [![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:javascript)
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush). - Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).

View File

@ -0,0 +1,59 @@
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,
}

View File

@ -32,6 +32,8 @@ 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
@ -42,6 +44,7 @@ Backends
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
@ -80,3 +83,4 @@ Backends
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

View File

@ -18,6 +18,7 @@ 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 -----------------------------------------------------
@ -50,6 +51,7 @@ 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.
@ -134,6 +136,11 @@ html_theme_options = {
'title': 'Firefox Extension', 'title': 'Firefox Extension',
'internal': False, 'internal': False,
}, },
{
'href': 'https://f-droid.org/en/packages/tech.platypush.platypush/',
'title': 'Android App',
'internal': False,
},
], ],
} }

View File

@ -28,7 +28,6 @@ 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

View File

@ -0,0 +1,5 @@
``platypush.backend.joystick.jstest``
=====================================
.. automodule:: platypush.backend.joystick.jstest
:members:

View File

@ -0,0 +1,5 @@
``platypush.backend.joystick.linux``
====================================
.. automodule:: platypush.backend.joystick.linux
:members:

View File

@ -0,0 +1,5 @@
``platypush.backend.music.spotify.connect``
===========================================
.. automodule:: platypush.backend.music.spotify.connect
:members:

View File

@ -0,0 +1,5 @@
``platypush.backend.zwave.mqtt``
================================
.. automodule:: platypush.backend.zwave.mqtt
:members:

View File

@ -1,5 +0,0 @@
``platypush.message.event.http.ota.booking``
============================================
.. automodule:: platypush.message.event.http.ota.booking
:members:

View File

@ -1,5 +0,0 @@
``platypush.plugins.http.request.ota.booking``
==============================================
.. automodule:: platypush.plugins.http.request.ota.booking
:members:

View File

@ -0,0 +1,5 @@
``platypush.plugins.pwm.pca9685``
=================================
.. automodule:: platypush.plugins.pwm.pca9685
:members:

View File

@ -1,6 +0,0 @@
``platypush.plugins.switch.switchbot``
======================================
.. automodule:: platypush.plugins.switch.switchbot
:members:

View File

@ -0,0 +1,5 @@
``platypush.plugins.switchbot.bluetooth``
=========================================
.. automodule:: platypush.plugins.switchbot.bluetooth
:members:

View File

@ -0,0 +1,5 @@
``platypush.plugins.switchbot``
===============================
.. automodule:: platypush.plugins.switchbot
:members:

View File

@ -1,5 +0,0 @@
``platypush.plugins.video.torrentcast``
=======================================
.. automodule:: platypush.plugins.video.torrentcast
:members:

View File

@ -0,0 +1,5 @@
``platypush.plugins.zwave._base``
=================================
.. automodule:: platypush.plugins.zwave._base
:members:

View File

@ -0,0 +1,5 @@
``platypush.plugins.zwave.mqtt``
================================
.. automodule:: platypush.plugins.zwave.mqtt
:members:

View File

@ -61,7 +61,6 @@ 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
@ -105,6 +104,7 @@ 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,9 +119,10 @@ 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
@ -137,7 +138,6 @@ 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/video.torrentcast.rst
platypush/plugins/weather.rst platypush/plugins/weather.rst
platypush/plugins/weather.buienradar.rst platypush/plugins/weather.buienradar.rst
platypush/plugins/weather.darksky.rst platypush/plugins/weather.darksky.rst
@ -147,3 +147,5 @@ Plugins
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

View File

@ -24,7 +24,7 @@ from .utils import set_thread_name
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>' __author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
__version__ = '0.20.7' __version__ = '0.21.1'
logger = logging.getLogger('platypush') logger = logging.getLogger('platypush')

View File

@ -6,10 +6,9 @@
import logging import logging
import re import re
import socket import socket
import threading
import time import time
from threading import Thread from threading import Thread, Event as ThreadEvent, get_ident
from typing import Optional, Dict from typing import Optional, Dict
from platypush.bus import Bus from platypush.bus import Bus
@ -62,7 +61,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 = threading.Event() self._stop_event = ThreadEvent()
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
@ -220,7 +219,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 = threading.get_ident() self.thread_id = 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

View File

@ -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: except Exception as e:
pass self.logger.debug('Not a number: {}: {}'.format(data, e))
self.bus.post(FeedUpdateEvent(feed=feed, data=data)) self.bus.post(FeedUpdateEvent(feed=feed, data=data))
return _handler return _handler

View File

@ -55,16 +55,17 @@ 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()) now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable]
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(tzinfo=gettz()) timestamp = datetime.datetime.fromisoformat(self.when).replace(
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
except (TypeError, ValueError): except (TypeError, ValueError):
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable]
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

View File

@ -13,37 +13,41 @@ 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
@ -52,20 +56,24 @@ 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
@ -75,24 +83,26 @@ 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: ...
@ -101,9 +111,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
@ -113,33 +123,34 @@ 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
@ -147,61 +158,66 @@ 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", {"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
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", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time}) self._client._send_command("CmdChangeModeParameters",
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
"auto_disconnect_time": self._auto_disconnect_time})
class FlicClient(asyncio.Protocol): 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"),
@ -212,7 +228,8 @@ 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", "bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"), ("EvtGetInfoResponse", "<B6sBBhBBH",
"bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"), ("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"), ("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtBluetoothControllerStateChange", "<B", "state"), ("EvtBluetoothControllerStateChange", "<B", "state"),
@ -223,9 +240,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 == None else struct.Struct(x[1]), _EVENTS)) _EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS)) _EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [ _COMMANDS = [
("CmdGetInfo", "", ""), ("CmdGetInfo", "", ""),
("CmdCreateScanner", "<I", "scan_id"), ("CmdCreateScanner", "<I", "scan_id"),
@ -239,168 +256,169 @@ 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, "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.
""" """
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"]) items["bd_addr"] = FlicClient._bdaddr_string_to_bytes()
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)
@ -409,83 +427,85 @@ 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] == None: if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is 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()
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_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())
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"])
@ -494,61 +514,60 @@ 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"], items["time_diff"]) channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"],
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"], 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.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):
def data_received(self,data): cdata = self.buffer + data
cdata=self.buffer+data self.buffer = b""
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

View File

@ -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 == None else struct.Struct(x[1]), _EVENTS)) _EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS)) _EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [ _COMMANDS = [
("CmdGetInfo", "", ""), ("CmdGetInfo", "", ""),
("CmdCreateScanner", "<I", "scan_id"), ("CmdCreateScanner", "<I", "scan_id"),
@ -245,17 +245,19 @@ 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()
@ -267,113 +269,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,
@ -381,47 +383,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)
@ -432,83 +434,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] == None: if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is 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"])
@ -521,44 +523,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]
@ -568,10 +570,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)
@ -579,7 +581,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)
@ -590,13 +592,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.
""" """

View File

@ -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, x_resolution=640, y_resolution=480, def __init__(self, listen_port, bind_address='0.0.0.0', 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,13 +49,17 @@ 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(('0.0.0.0', self.listen_port)) self.server_socket.bind((self.bind_address, self.listen_port))
self.server_socket.listen(0) self.server_socket.listen(0)
import picamera import picamera
@ -134,13 +138,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: except Exception as e:
pass self.logger.warning('Could not stop recording: {}'.format(str(e)))
try: try:
connection.close() connection.close()
except: except Exception as e:
pass self.logger.warning('Could not close connection: {}'.format(str(e)))
self.send_camera_action(self.CameraAction.START_RECORDING) self.send_camera_action(self.CameraAction.START_RECORDING)

View File

@ -51,11 +51,10 @@ 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: except Exception as e:
pass self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e)))
msg.ack() msg.ack()
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data)) self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))

View File

@ -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: except Exception as e:
pass logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e)))
event = WebhookEvent(**event_args) event = WebhookEvent(**event_args)

View File

@ -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) return redirect(redirect_page, 302) # lgtm [py/url-redirection]
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) redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
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

View File

@ -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) redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
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

View File

@ -45,14 +45,12 @@ 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: except (ValueError, TypeError):
# noinspection PyBroadException
try: try:
v = float(v) v = float(v)
except: except (ValueError, TypeError):
pass pass
kwargs[k] = v kwargs[k] = v

View File

@ -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: except Exception as e:
abort(400, 'Invalid JSON request') abort(400, 'Invalid JSON request: {}'.format(str(e)))
source = args.get('source') source = args.get('source')
if not source: if not source:

View File

@ -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: with open(fifo, 'rb') as f: # lgtm [py/path-injection]
send_header = True send_header = True
while True: while True:

View File

@ -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) return redirect(redirect_page, 302) # lgtm [py/url-redirection]
if user_manager.get_user_count() > 0: if user_manager.get_user_count() > 0:
return redirect('/login?redirect=' + redirect_page, 302) return redirect('/login?redirect=' + redirect_page, 302) # lgtm [py/url-redirection]
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) redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
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

View File

@ -123,7 +123,8 @@ def _authenticate_token():
try: try:
user_manager.validate_jwt_token(user_token) user_manager.validate_jwt_token(user_token)
return True return True
except: except Exception as e:
logger().debug(str(e))
return token and user_token == token return token and user_token == token

View File

@ -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 ("get_new_items must be implemented in a derived class") raise NotImplementedError("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('\$\{\s*(.*)\s*\}', self.path) m = re.match(r'\${\s*(.*)\s*}', self.path)
response = eval(m.group(1)) response = eval(m.group(1))
for entry in response: for entry in response:

View File

@ -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':
import weasyprint from weasyprint import HTML, CSS
from weasyprint.fonts import FontConfiguration from weasyprint.fonts import FontConfiguration
body_style = 'body { {body_style} }'.format(body_style=self.body_style) body_style = 'body { ' + self.body_style + ' }'
font_config = FontConfiguration() font_config = FontConfiguration()
css = [weasyprint.CSS('https://fonts.googleapis.com/css?family=Merriweather'), css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'),
weasyprint.CSS(string=body_style, font_config=font_config)] CSS(string=body_style, font_config=font_config)]
weasyprint.HTML(string=content).write_pdf(digest_filename, stylesheets=css) 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))

View File

@ -50,15 +50,13 @@ 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)
@ -92,10 +90,11 @@ 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)))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More