Compare commits

...

136 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
Fabio Manganiello eb486df1ee Bump version: 0.20.6 → 0.20.7 2021-03-26 23:09:33 +01:00
Fabio Manganiello a4c70f1e4d Fixed typo in README 2021-03-26 01:46:31 +01:00
Fabio Manganiello c16f8aa39e Added mobile app section to the README 2021-03-26 01:44:54 +01:00
Fabio Manganiello 570f1d0cf6 Passing expire_on_commit=False on sessionmaker() [see #181]
Accessing db objects outside of their session seems to fail on SQLAlchemy >= 1.4
with a `Instance `Instance <x> is not bound to a Session` error.

Setting expire_on_commit=False on the session seems to somehow fix the issue
(see https://stackoverflow.com/questions/3039567/sqlalchemy-detachedinstanceerror-with-regular-attribute-not-a-relation)
2021-03-25 20:30:51 +01:00
Fabio Manganiello 4313b6e883 media.vlc.status should synchronize on _stop_lock, or it may fail in the middle of its execution if the VLC session is being freed 2021-03-24 15:02:05 +01:00
Fabio Manganiello 00fabf3853 Reverted MQTT client reconnection logic until I find a more reliable way to identify the errors that caused the disconnections 2021-03-22 02:11:46 +01:00
Fabio Manganiello cad184fc1f MQTT_ERR_NOMEM should not result in a reconnection 2021-03-22 02:07:53 +01:00
Fabio Manganiello 928bb3667a Reconnection logic for MQTT disconnections caused by temporary errors 2021-03-22 01:52:27 +01:00
Fabio Manganiello 782be7794b More robust logic to deal with broken lines in HTTP logs 2021-03-21 10:12:27 +01:00
Fabio Manganiello 40dc739d09 Even more robust logic in case of missing HTTP version on the logged request - if anything is wrong with the format simply default to http_version = 1.0 2021-03-18 14:02:25 +01:00
Fabio Manganiello 4821fe086b More robust logic in case of missing HTTP version on the logged request 2021-03-18 11:30:57 +01:00
Fabio Manganiello 8d621b2688 Updated CHANGELOG 2021-03-18 01:35:03 +01:00
Fabio Manganiello 1355f7a3f6 [Dashboards] The class value should only apply to the widget wrapper, not to the wrapped widget [see #179] 2021-03-18 01:30:29 +01:00
Fabio Manganiello 3ce98305f0 Support for on_moved handler on file/log monitor backends 2021-03-17 23:21:52 +01:00
Fabio Manganiello 0a4cadba3e Fixed KeyError 2021-03-17 01:53:10 +01:00
Fabio Manganiello 9e46ab0b60 Wait for _on_stop_event in media.vlc.stop before releasing the instance 2021-03-16 22:34:03 +01:00
Fabio Manganiello c74d2fb124 Bump version: 0.20.5 → 0.20.6 2021-03-16 21:33:08 +01:00
Fabio Manganiello ca573cb980 Added possibility to pass id instead of name to the switch mixin toggle method 2021-03-16 21:03:58 +01:00
Fabio Manganiello 75deb0393d Fixed class name typo 2021-03-16 20:56:34 +01:00
Fabio Manganiello 14f1c44378 - If a Z-Wave event includes an explicit value update then explicitly set that value on the node, in order to prevent issues with Z-Wave value updates not yet propagated to the node structure
- Added Z-Wave switch component
2021-03-16 20:52:30 +01:00
Fabio Manganiello fdd46edb6a Do not force type cohercion on bool values - !!parseInt(true) = false in JavaScript, for some reason 2021-03-16 20:11:49 +01:00
Fabio Manganiello b9738d88df Added some temporary debug lines to Z-Wave's Value.vue to investigate setValue issues 2021-03-16 20:00:25 +01:00
Fabio Manganiello f92d19a24e media.vlc.stop should be synchronized on the _stop_lock and should call _reset_state instead of simply setting self._player = None 2021-03-16 19:42:59 +01:00
Fabio Manganiello ce0ca2e9ee Explicitly pass the new value on the Z-Wave setValue handler in case of binary toggles, since the target element can't be reliably determined 2021-03-16 19:37:42 +01:00
Fabio Manganiello 7f157d0234 Dockerfile should be part of the examples/ folder 2021-03-16 00:29:52 +01:00
Fabio Manganiello 8a3df30001 Removed Travis-CI and ReadTheDocs integrations (replaced by Platypush CI hooks) 2021-03-16 00:28:25 +01:00
Fabio Manganiello 82274d3d12 Added updated reference to CONTRIBUTING.md in README 2021-03-16 00:20:21 +01:00
Fabio Manganiello ab6c85c2a6 Updated CHANGELOG 2021-03-16 00:17:20 +01:00
Fabio Manganiello f8564c19cd Added log.http backend to monitor HTTP logs [closes #167] 2021-03-16 00:03:32 +01:00
Fabio Manganiello bf519babb0 Updated tests and docs build badges 2021-03-14 17:35:25 +01:00
Fabio Manganiello 632a7ab792 Added README for auto-generated docs and changed theme to sphinx-material 2021-03-14 15:07:10 +01:00
Fabio Manganiello 1d3d741212 Fixed Sphinx build warnings 2021-03-14 01:09:01 +01:00
Fabio Manganiello b171cb1012 A click on a Z-Wave binary event should result in the value being toggled, not rewritten [see #176] 2021-03-14 01:05:02 +01:00
Fabio Manganiello 352d421e61 Added file.monitor backend [closes #172]
The file.monitor backend leverages watchdog instead of the Linux-only
inotify API and it replaces the inotify backend.
2021-03-14 00:08:20 +01:00
Fabio Manganiello 6f224cbda9 Removed legacy "local" backend and pusher script 2021-03-13 01:52:15 +01:00
Fabio Manganiello adb472da7f Messages should always be posted to the Redis bus in valid JSON format - no need for ast.literal_eval fallback 2021-03-13 01:52:15 +01:00
Fabio Manganiello 347a4d2555 disable_logging=True for ZwaveNodeGroupEvent 2021-03-12 20:29:26 +01:00
Fabio Manganiello cef310ffd7 Added missing methods docs 2021-03-12 01:53:16 +01:00
Fabio Manganiello 96588df83b Bump version: 0.20.4 → 0.20.5 2021-03-12 01:40:03 +01:00
Fabio Manganiello 02f6845e72 - Added weather.openweathermap plugin and backend, as a replacement for Darksky [closes #177]
- Added note to the Darksky plugin about the decomissioning of the API by the end of the year
2021-03-12 01:31:26 +01:00
Fabio Manganiello 40834f7ce5 The zigbee2mqtt backend should use a suffix for the default client_id to prevent clashes with the default client_id from the MQTT backend [see #175] 2021-03-11 19:24:13 +01:00
Fabio Manganiello 3d6af00ee6 Disable logging of ZwaveValueEvent objects, as they tend to be very verbose 2021-03-11 17:53:39 +01:00
Fabio Manganiello b06867dc5d Added CONTRIBUTING file 2021-03-11 01:50:24 +01:00
Fabio Manganiello 22dad79dd5
Merge pull request #166 from BlackLight/dependabot/npm_and_yarn/platypush/backend/http/webapp/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4 in /platypush/backend/http/webapp
2021-03-10 11:38:07 +01:00
Fabio Manganiello 211025cedb The redis_queue parameter is not necessarily defined on the app config 2021-03-10 11:15:06 +01:00
dependabot[bot] da27ed7546
Bump elliptic from 6.5.3 to 6.5.4 in /platypush/backend/http/webapp
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-10 10:00:42 +00:00
Fabio Manganiello 775478fff0 Improvement management of Z-Wave value types from the web panel 2021-03-10 10:58:51 +01:00
Fabio Manganiello 093bac3a60 - Addressed comments in #174
- Replaced active_scan flag with a list of track_devices
2021-03-09 19:03:04 +01:00
Fabio Manganiello b3606a8ac3 Changelog edit 2021-03-09 12:54:43 +01:00
Fabio Manganiello 4902475caf Added active_scan mode to bluetooth.scanner backend to actively perform a lookup name on each device discovered at least once [see #174] 2021-03-09 11:50:59 +01:00
Fabio Manganiello 7687e52058 Added cron examples to README.md 2021-03-09 01:43:28 +01:00
Fabio Manganiello eae4b4f62a The tmp_file fixture destructor should clean up any temporary files (in case multiple tests import it) 2021-03-09 00:22:18 +01:00
Fabio Manganiello 296458ece3 Cron expressions should follow the machine local time, not UTC [closes #173] 2021-03-09 00:18:33 +01:00
Fabio Manganiello 71af6e87e0 Bump version: 0.20.3 → 0.20.4 2021-03-08 01:51:39 +01:00
Fabio Manganiello c659ec507f Added tests/__main__.py entry point to run all the tests 2021-03-06 20:13:38 +01:00
Fabio Manganiello fb1953ce34 Using tests status badge from ci.platypush.tech on README instead of the Travis-CI status badge 2021-03-06 20:04:58 +01:00
Fabio Manganiello 34a108bbcb Clear log objects on teardown to prevent pytest logging errors 2021-03-06 20:02:25 +01:00
Fabio Manganiello 5be6ca50f0 Travis-CI pipeline migrated to pytest 2021-03-06 19:50:22 +01:00
Fabio Manganiello 090e7d6de8 Support for specifying the application Redis queue from the command line or service constructor 2021-03-06 19:22:13 +01:00
Fabio Manganiello 6f85318868 Use another Redis queue for the test app to prevent clashes with another Platypush service running on the same machine 2021-03-06 17:17:55 +01:00
Fabio Manganiello 8f256e4077 Check for file creation and content multiple times with timeout 2021-03-06 17:09:40 +01:00
Fabio Manganiello 4ed80a0945 More tests improvements 2021-03-06 17:03:50 +01:00
Fabio Manganiello ca90060ba1 Tests improvements 2021-03-06 16:25:37 +01:00
Fabio Manganiello 49ad3261f1 Refactored tests to use pytest with fixtures instead of unittest.TestCase 2021-03-06 16:21:28 +01:00
Fabio Manganiello 73e16fa6b1 Updated web app files 2021-03-05 21:43:16 +01:00
Fabio Manganiello d860d8aef1 Added SmartThings switch web plugin 2021-03-05 21:29:32 +01:00
Fabio Manganiello 36aee6f787 The same applies to procedures - don't dump context as a JSON in case of exceptions 2021-03-05 19:00:26 +01:00
Fabio Manganiello 04ff008800 Context shouldn't be dumped as json in case of exceptions - if some objects are not JSON serializable then we end up with an uncaught exception 2021-03-05 18:52:57 +01:00
Fabio Manganiello 47ba13d985 calendar.ical.get_upcoming_events should fail hard if there was any exception/unsuccessful response
The error should not be swallowed and it should be instead propagated up
to calendar.get_upcoming_events, if it's called from that context. And
calendar.get_upcoming_events should be in charge of handling the
exceptions and make sure that failing to retrieve the events for one
calendar doesn't make the whole method fail.
2021-03-05 11:16:14 +01:00
Fabio Manganiello 4ada1c663d Added SmartThings plugin [#148] 2021-03-05 02:23:28 +01:00
Fabio Manganiello 210cefc1a4 General improvements on the Zeroconf plugin and backend 2021-03-03 19:20:12 +01:00
Fabio Manganiello e43147e6a3 Added CHANGELOG 2021-02-28 23:54:22 +01:00
636 changed files with 27406 additions and 6296 deletions

1
.gitignore vendored
View File

@ -18,5 +18,4 @@ platypush/notebooks
platypush/requests
/http-client.env.json
/platypush/backend/http/static/css/dist
/tests/etc/scripts
/tests/etc/dashboards

View File

@ -1,7 +0,0 @@
build:
image: latest
python:
version: 3.7
setup_py_install: true

View File

@ -1,11 +0,0 @@
pyyaml
requests
flask
redis
python-dateutil
websockets
bcrypt
sqlalchemy
croniter
zeroconf>=0.27.0
pyjwt

View File

@ -1,22 +0,0 @@
language: python
dist: xenial
python:
- "3.7"
install: "pip install -r .travis.requirements"
script: ./run_tests.sh
notifications:
email:
recipients:
- blacklight86@gmail.com
on_success: change
on_failure: change
services:
- redis-server
git:
submodules: false

177
CHANGELOG.md Normal file
View File

@ -0,0 +1,177 @@
# Changelog
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.
## [Unreleased]
- Added `music.spotify.connect` backend to emulate a Spotify Connect receiver through Platypush.
## [0.21.1] - 2021-06-22
### Added
- Added `switchbot` plugin to interact with Switchbot devices over the cloud API instead of
directly accessing the device's Bluetooth interface.
- Added `marshmallow` dependency - it will be used from now own to dump and document schemas
and responses instead of the currently mixed approach with `Response` objects and plain
dictionaries and lists.
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
- Added generic joystick backend `backend.joystick.jstest` which uses `jstest` from the
standard `joystick` system package to read the state of joysticks not compatible with
`python-inputs`.
- Added PWM PCA9685 plugin.
- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where
``python-inputs`` doesn't work and ``jstest`` is too slow.
### Changed
- `switch.switchbot` plugin renamed to `switchbot.bluetooth` plugin, while the new plugin
that uses the Switchbot API is simply named `switchbot`.
### Fixed
- More robust reconnection logic on the Pushbullet backend in case of websocket errors.
## [0.21.0] - 2021-05-06
### Added
- Support for custom PopcornTime API mirror/base URL.
- Full support for TV series search.
### Fixed
- Fixed torrent search (now using a different PopcornTime API mirror).
- Migrated SASS engine from `node-sass` (currently deprecated and broken on Node 16) to `sass`.
- Fixed alignment of Z-Wave UI header on Chrome/Webkit.
## [0.20.10] - 2021-04-28
### Fixed
- Fixed zwave/zwavejs2mqtt interoperability.
## [0.20.9] - 2021-04-12
### Added
- Added zwavejs2mqtt integration (see [#186](https://git.platypush.tech/platypush/platypush/-/issues/186).
### Fixed
- Major LINT fixes.
### Removed
- Removed unmaintained integrations: TorrentCast and Booking.com
## [0.20.8] - 2021-04-04
### Added
- Added `<Camera>` dashboard widget.
- Added support for custom dashboard widgets with customized (see https://git.platypush.tech/platypush/platypush/-/wikis/Backends#creating-custom-widgets).
- Added support for controls on `music.mpd` dashboard widget.
### Fixed
- Fixed zigbee2mqtt backend error in case of messages with empty payload (see [#184](https://git.platypush.tech/platypush/platypush/-/issues/184)).
- Fixed compatibility with all versions of websocket-client - versions >= 0.58.0 pass a `WebSocketApp` object as a first
argument to the callbacks, as well as versions < 0.54.0 do, but the versions in between don't pass this argument.
## [0.20.7] - 2021-03-26
### Fixed
- Fixed race condition on `media.vlc.stop` when clearing the VLC instance.
- Fixed dashboard widgets custom classes being propagated both to the container and to the widget content [see #179]
- Fixed compatibility with SQLAlchemy >= 1.4.
## [0.20.6] - 2021-03-16
### Added
- Added `log.http` backend to monitor changes to HTTP log files
(see [#167](https://git.platypush.tech/platypush/platypush/-/issues/167)).
- Added `file.monitor` backend, which replaces the `inotify` backend
(see [#172](https://git.platypush.tech/platypush/platypush/-/issues/172)).
### Removed
- Removed legacy `pusher` script and `local` backend.
### Fixed
- Fixed support for Z-Wave switches.
- Fixed possible race condition on VLC stop.
## [0.20.5] - 2021-03-12
### Added
- Added support for a static list of devices to actively scan to the `bluetooth.scanner` backend
(see [#174](https://git.platypush.tech/platypush/platypush/-/issues/174)).
- Added `weather.openweathermap` plugin and backend, which replaces `weather.darksky`, since the
Darksky API will be completely shut down by the end of 2021.
### Fixed
- Cron expressions should adhere to the UNIX cronjob standard and use the machine local time,
not UTC, as a reference (closes [#173](https://git.platypush.tech/platypush/platypush/-/issues/173)).
- Better management of Z-Wave values types from the UI.
- Disable logging for `ZwaveValueEvent` events - they tend to be very verbose and
can impact the performance on slower devices. They will still be published to the
websocket clients though, so you can still debug Z-Wave values issues from the browser
developer console (enable debug traces).
- Added suffix to the `zigbee.mqtt` backend default `client_id` to prevent clashes with
the default `mqtt` backend `client_id`.
## [0.20.4] - 2021-03-08
### Added
- Added SmartThings integration.
- Support for custom Redis message queue name over the `--redis-queue` argument.
### Fixed
- Refactored tests to use `pytest` instead of `unittest`.
- Some major bug fixes on procedures and hooks context evaluation.
## [0.20.3] - 2021-02-28
### Fixed
- Several bug fixes on the VLC plugin, including proper management of stop/end-of-stream, volume set and missing integration requirements in `requirements.txt` and `setup.py`.
## [0.20.2] - 2021-02-27
### Fixed
- More stable ZeroConf backends registration logic in case of partial or missing results.
- Improved and refactored integration tests.
### Added
- Support for passing context variables (${}) from YAML procedures/hooks/crons to Python procedure/hooks/crons.
- New integration test for testing procedures.

37
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,37 @@
Thanks for considering contributing your work to make Platypush a better product!
Contributions are more than welcome, and the follow the standard Gitlab procedure:
- [Fork the repo](https://git.platypush.tech/platypush/platypush).
- Prepare your changes.
- [Submit a merge request](https://git.platypush.tech/platypush/platypush/-/merge_requests).
Guidelines:
- The code should ideally have no LINT warnings/issues.
- Project conventions:
- 4 spaces to indent.
- RST format for classes and methods documentation
- Run `python generate_missing_docs.py` if you are adding new plugins/backends to automatically
generate the doc templates. Make sure that you don't accidentally remove lines elements from
the docs because of missing dependencies on the machine you use to generate the docs.
- Naming conventions: plugin classes are named `<Module>Plugin` and backend classes are
named `<Module>Backend`, with `<Module>` being the (camel-case) representation of the
Python module of the plugin without the prefix - for example, the plugin under
`platypush.plugins.light.hue` must be named `LightHuePlugin`.
- If possible, [add a test](https://git.platypush.tech/platypush/platypush/-/tree/master/tests)
for the new functionality. If you have built a new functionality that works with some specific
device or service then it's not required to write a test that mocks the whole service, but if
you are changing some of the core entities (e.g. requests, events, procedures, hooks, crons
or the bus) then make sure to add tests and not to break the existing tests.
- If the feature requires an optional dependency then make sure to document it:
- In the class docstring (see other plugins and backends for examples)
- In [`setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72) as
an `extras_require` entry
- In [`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt) -
if the feature is optional then leave it commented and add a one-line comment to explain which
plugin or backend requires it.

View File

@ -1,12 +1,14 @@
Platypush
=========
[![Build Status](https://travis-ci.org/BlackLight/platypush.svg?branch=master)](https://travis-ci.org/BlackLight/platypush)
[![Documentation Status](https://readthedocs.org/projects/platypush/badge/?version=latest)](https://docs.platypush.tech/en/latest/)
[![Build Status](https://ci.platypush.tech/status.svg)](https://ci.platypush.tech/latest.log)
[![Documentation Status](https://ci.platypush.tech/docs/status.svg)](https://ci.platypush.tech/docs/latest.log)
[![pip version](https://img.shields.io/pypi/v/platypush.svg?style=flat)](https://pypi.python.org/pypi/platypush/)
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/issues)
[![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).
@ -14,7 +16,7 @@ Platypush
- The [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
- Extensive documentation for all the available integrations and messages is available on [ReadTheDocs](https://docs.platypush.tech/en/latest/).
- Extensive documentation for all the available integrations and messages [is available](https://docs.platypush.tech/).
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
@ -277,6 +279,48 @@ curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_
}' http://host:8008/execute
```
### Cronjobs
Cronjobs are pieces of logic that will be run at regular intervals, expressed in crontab-compatible syntax.
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` as
functions labelled by the `@cron` decorator.
Note that seconds are also supported (unlike the standard crontab definition), but, for back-compatibility with the
standard crontab format, they are at the end of the cron expression, so the expression is actually in the format
`<minute> <hour> <day_of_month> <month> <day_of_week> <second>`.
YAML example for a cronjob that is executed every 30 seconds and checks if a Bluetooth device is nearby:
```yaml
cron.check_bt_device:
cron_expression: '* * * * * */30'
actions:
- action: bluetooth.lookup_name
args:
addr: XX:XX:XX:XX:XX:XX
- if ${name}:
- action: procedure.on_device_on
- else:
- action: procedure.on_device_off
```
Python example:
```python
# Content of ~/.config/platypush/scripts/bt_cron.py
from platypush.cron import cron
from platypush.utils import run
@cron('* * * * * */30')
def check_bt_device(**context):
name = run('bluetooth.lookup_name').get('name')
if name:
# on_device_on logic here
else:
# on_device_off logic here
```
### The web interface
If [`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html) is enabled then a web interface
@ -432,12 +476,23 @@ platydock stop device_id
4. Remove the instance:
```shell
platyvdock rm device_id
platydock rm device_id
```
## Mobile app
An [official Android app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on the F-Droid store.
It allows to easily discover and manage multiple Platypush services on a network through the web interface, and it
easily brings the power of Platypush to your fingertips.
## Tests
To run the tests run the `run_tests.sh` from the source root folder - no further configuration is required.
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
Or run the following command from the project root folder:
```shell
python -m tests
```
---

21
docs/README.md Normal file
View File

@ -0,0 +1,21 @@
# Platypush self-generated reference
====================================
This directory contains the Sphinx self-generated documentation for Platypush.
Dependencies required to generate the documentation:
```shell
$ [sudo] pip install sphinx 'git+https://github.com/bashtage/sphinx-material.git'
```
To generate the HTML documentation:
```shell
$ make html
```
The output will be generated under `build/html`.
Type `make` with no additional arguments to get a full list of the supported output formats.

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

@ -22,6 +22,7 @@ Backends
platypush/backend/clipboard.rst
platypush/backend/covid19.rst
platypush/backend/dbus.rst
platypush/backend/file.monitor.rst
platypush/backend/foursquare.rst
platypush/backend/github.rst
platypush/backend/google.fit.rst
@ -31,16 +32,19 @@ Backends
platypush/backend/http.poll.rst
platypush/backend/inotify.rst
platypush/backend/joystick.rst
platypush/backend/joystick.jstest.rst
platypush/backend/joystick.linux.rst
platypush/backend/kafka.rst
platypush/backend/light.hue.rst
platypush/backend/linode.rst
platypush/backend/local.rst
platypush/backend/log.http.rst
platypush/backend/mail.rst
platypush/backend/midi.rst
platypush/backend/mqtt.rst
platypush/backend/music.mopidy.rst
platypush/backend/music.mpd.rst
platypush/backend/music.snapcast.rst
platypush/backend/music.spotify.connect.rst
platypush/backend/nextcloud.rst
platypush/backend/nfc.rst
platypush/backend/nodered.rst
@ -71,9 +75,12 @@ Backends
platypush/backend/todoist.rst
platypush/backend/travisci.rst
platypush/backend/trello.rst
platypush/backend/weather.rst
platypush/backend/weather.buienradar.rst
platypush/backend/weather.darksky.rst
platypush/backend/weather.openweathermap.rst
platypush/backend/websocket.rst
platypush/backend/wiimote.rst
platypush/backend/zigbee.mqtt.rst
platypush/backend/zwave.rst
platypush/backend/zwave.mqtt.rst

View File

@ -18,12 +18,13 @@ import sys
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath("./_ext"))
# -- Project information -----------------------------------------------------
project = 'platypush'
copyright = '2017-2020, Fabio Manganiello'
project = 'Platypush'
copyright = '2017-2021, Fabio Manganiello'
author = 'Fabio Manganiello'
# The short X.Y version
@ -50,6 +51,7 @@ extensions = [
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'sphinx_rtd_theme',
'sphinx_marshmallow',
]
# Add any paths that contain templates here, relative to this directory.
@ -86,7 +88,8 @@ pygments_style = 'sphinx'
# a list of builtin themes.
#
# html_theme = 'haiku'
html_theme = 'sphinx_rtd_theme'
# html_theme = 'sphinx_rtd_theme'
html_theme = 'sphinx_material'
html_domain_indices = True
@ -94,7 +97,52 @@ html_domain_indices = True
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
html_theme_options = {
'nav_title': 'Platypush documentation',
'repo_url': 'https://git.platypush.tech/platypush/platypush',
'repo_name': 'Source code',
'repo_type': 'gitlab',
'color_primary': 'green',
'color_accent': 'light-green',
'logo_icon': '&#128366',
'nav_links': [
{
'href': 'https://platypush.tech/',
'title': 'Homepage',
'internal': False,
},
{
'href': 'https://blog.platypush.tech/',
'title': 'Blog',
'internal': False,
},
{
'href': 'https://git.platypush.tech/platypush/platypush',
'title': 'Repository',
'internal': False,
},
{
'href': 'https://git.platypush.tech/platypush/platypush/-/wikis/home',
'title': 'Wiki',
'internal': False,
},
{
'href': 'https://chrome.google.com/webstore/detail/platypush/aphldjclndofhflbbdnmpejbjgomkbie',
'title': 'Chrome Extension',
'internal': False,
},
{
'href': 'https://addons.mozilla.org/en-US/firefox/addon/platypush/',
'title': 'Firefox Extension',
'internal': False,
},
{
'href': 'https://f-droid.org/en/packages/tech.platypush.platypush/',
'title': 'Android App',
'internal': False,
},
],
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@ -109,8 +157,9 @@ html_domain_indices = True
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
html_sidebars = {
'**': ['logo-text.html', 'globaltoc.html', 'localtoc.html', 'searchbox.html']
}
# -- Options for HTMLHelp output ---------------------------------------------
@ -263,6 +312,9 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
'RPi.GPIO',
'RPLCD',
'imapclient',
'pysmartthings',
'aiohttp',
'watchdog',
]
sys.path.insert(0, os.path.abspath('../..'))
@ -275,3 +327,5 @@ def skip(app, what, name, obj, skip, options):
def setup(app):
app.connect("autodoc-skip-member", skip)
# vim:sw=4:ts=4:et:

View File

@ -18,6 +18,7 @@ Events
platypush/events/covid19.rst
platypush/events/custom.rst
platypush/events/distance.rst
platypush/events/file.rst
platypush/events/foursquare.rst
platypush/events/geo.rst
platypush/events/github.rst
@ -27,13 +28,13 @@ Events
platypush/events/gps.rst
platypush/events/http.rst
platypush/events/http.hook.rst
platypush/events/http.ota.booking.rst
platypush/events/http.rss.rst
platypush/events/inotify.rst
platypush/events/joystick.rst
platypush/events/kafka.rst
platypush/events/light.rst
platypush/events/linode.rst
platypush/events/log.http.rst
platypush/events/mail.rst
platypush/events/media.rst
platypush/events/midi.rst

View File

@ -0,0 +1,5 @@
``platypush.backend.file.monitor``
==================================
.. automodule:: platypush.backend.file.monitor
:members:

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

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

View File

@ -0,0 +1,5 @@
``platypush.backend.log.http``
==============================
.. automodule:: platypush.backend.log.http
: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.weather.openweathermap``
============================================
.. automodule:: platypush.backend.weather.openweathermap
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
``platypush.plugins.config``
============================
.. automodule:: platypush.plugins.config
: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

@ -0,0 +1,5 @@
``platypush.plugins.smartthings``
=================================
.. automodule:: platypush.plugins.smartthings
: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.weather.openweathermap``
============================================
.. automodule:: platypush.plugins.weather.openweathermap
:members:

View File

@ -0,0 +1,5 @@
``platypush.plugins.weather``
=============================
.. automodule:: platypush.plugins.weather
: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

@ -27,6 +27,7 @@ Plugins
platypush/plugins/camera.pi.rst
platypush/plugins/chat.telegram.rst
platypush/plugins/clipboard.rst
platypush/plugins/config.rst
platypush/plugins/covid19.rst
platypush/plugins/csv.rst
platypush/plugins/db.rst
@ -60,7 +61,6 @@ Plugins
platypush/plugins/graphite.rst
platypush/plugins/homeseer.rst
platypush/plugins/http.request.rst
platypush/plugins/http.request.ota.booking.rst
platypush/plugins/http.request.rss.rst
platypush/plugins/http.webpage.rst
platypush/plugins/ifttt.rst
@ -104,12 +104,14 @@ Plugins
platypush/plugins/ping.rst
platypush/plugins/printer.cups.rst
platypush/plugins/pushbullet.rst
platypush/plugins/pwm.pca9685.rst
platypush/plugins/qrcode.rst
platypush/plugins/redis.rst
platypush/plugins/rtorrent.rst
platypush/plugins/sensor.rst
platypush/plugins/serial.rst
platypush/plugins/shell.rst
platypush/plugins/smartthings.rst
platypush/plugins/sound.rst
platypush/plugins/ssh.rst
platypush/plugins/stt.rst
@ -117,9 +119,10 @@ Plugins
platypush/plugins/stt.picovoice.hotword.rst
platypush/plugins/stt.picovoice.speech.rst
platypush/plugins/switch.rst
platypush/plugins/switch.switchbot.rst
platypush/plugins/switch.tplink.rst
platypush/plugins/switch.wemo.rst
platypush/plugins/switchbot.rst
platypush/plugins/switchbot.bluetooth.rst
platypush/plugins/system.rst
platypush/plugins/tcp.rst
platypush/plugins/tensorflow.rst
@ -135,11 +138,14 @@ Plugins
platypush/plugins/user.rst
platypush/plugins/utils.rst
platypush/plugins/variable.rst
platypush/plugins/video.torrentcast.rst
platypush/plugins/weather.rst
platypush/plugins/weather.buienradar.rst
platypush/plugins/weather.darksky.rst
platypush/plugins/weather.openweathermap.rst
platypush/plugins/websocket.rst
platypush/plugins/wiimote.rst
platypush/plugins/zeroconf.rst
platypush/plugins/zigbee.mqtt.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>'
__version__ = '0.20.3'
__version__ = '0.21.1'
logger = logging.getLogger('platypush')
@ -41,6 +41,9 @@ class Daemon:
# - plugins will post the responses they process
bus = None
# Default bus queue name
_default_redis_queue = 'platypush/bus'
pidfile = None
# backend_name => backend_obj map
@ -50,7 +53,7 @@ class Daemon:
n_tries = 2
def __init__(self, config_file=None, pidfile=None, requests_to_process=None,
no_capture_stdout=False, no_capture_stderr=False):
no_capture_stdout=False, no_capture_stderr=False, redis_queue=None):
"""
Constructor
Params:
@ -64,6 +67,7 @@ class Daemon:
capture by the logging system
no_capture_stderr -- Set to true if you want to disable the stderr
capture by the logging system
redis_queue -- Name of the (Redis) queue used for dispatching messages (default: platypush/bus).
"""
if pidfile:
@ -71,12 +75,15 @@ class Daemon:
with open(self.pidfile, 'w') as f:
f.write(str(os.getpid()))
self.redis_queue = redis_queue or self._default_redis_queue
self.config_file = config_file
Config.init(self.config_file)
logging.basicConfig(**Config.get('logging'))
redis_conf = Config.get('backend.redis') or {}
self.bus = RedisBus(on_message=self.on_message(), **redis_conf.get('redis_args', {}))
self.bus = RedisBus(redis_queue=self.redis_queue, on_message=self.on_message(),
**redis_conf.get('redis_args', {}))
self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr
self.event_processor = EventProcessor()
@ -108,11 +115,17 @@ class Daemon:
help="Set this flag if you have max stack depth " +
"exceeded errors so stderr won't be captured by " +
"the logging system")
parser.add_argument('--redis-queue', dest='redis_queue',
required=False, action='store_true',
default=cls._default_redis_queue,
help="Name of the Redis queue to be used to internally deliver messages "
"(default: platypush/bus)")
opts, args = parser.parse_known_args(args)
return cls(config_file=opts.config, pidfile=opts.pidfile,
no_capture_stdout=opts.no_capture_stdout,
no_capture_stderr=opts.no_capture_stderr)
no_capture_stderr=opts.no_capture_stderr,
redis_queue=opts.redis_queue)
def on_message(self):
"""

View File

@ -6,11 +6,10 @@
import logging
import re
import socket
import threading
import time
from threading import Thread
from typing import Optional
from threading import Thread, Event as ThreadEvent, get_ident
from typing import Optional, Dict
from platypush.bus import Bus
from platypush.config import Config
@ -62,7 +61,7 @@ class Backend(Thread, EventGenerator):
self.poll_seconds = float(poll_seconds) if poll_seconds else None
self.device_id = Config.get('device_id')
self.thread_id = None
self._stop_event = threading.Event()
self._stop_event = ThreadEvent()
self._kwargs = kwargs
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
self.zeroconf = None
@ -220,7 +219,7 @@ class Backend(Thread, EventGenerator):
def run(self):
""" Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """
self.thread_id = threading.get_ident()
self.thread_id = get_ident()
set_thread_name(self._thread_name)
if not callable(self.loop):
return
@ -313,9 +312,31 @@ class Backend(Thread, EventGenerator):
s.close()
return addr
def register_service(self, port: Optional[int] = None, name: Optional[str] = None, udp: bool = False):
def register_service(self,
port: Optional[int] = None,
name: Optional[str] = None,
srv_type: Optional[str] = None,
srv_name: Optional[str] = None,
udp: bool = False,
properties: Optional[Dict] = None):
"""
Initialize the Zeroconf service configuration for this backend.
:param port: Service listen port (default: the backend ``port`` attribute if available, or ``None``).
:param name: Service short name (default: backend name).
:param srv_type: Service type (default: ``_platypush-{name}._{proto}.local.``).
:param srv_name: Full service name (default: ``{hostname or device_id}.{type}``).
:param udp: Set to True if this is a UDP service.
:param properties: Extra properties to be passed on the service. Default:
.. code-block:: json
{
"name": "Platypush",
"vendor": "Platypush",
"version": "{platypush_version}"
}
"""
try:
from zeroconf import ServiceInfo, Zeroconf
@ -329,11 +350,12 @@ class Backend(Thread, EventGenerator):
'name': 'Platypush',
'vendor': 'Platypush',
'version': __version__,
**(properties or {}),
}
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
srv_type = '_platypush-{name}._{proto}.local.'.format(name=name, proto='udp' if udp else 'tcp')
srv_name = '{host}.{type}'.format(host=self.device_id, type=srv_type)
srv_type = srv_type or '_platypush-{name}._{proto}.local.'.format(name=name, proto='udp' if udp else 'tcp')
srv_name = srv_name or '{host}.{type}'.format(host=self.device_id, type=srv_type)
if port:
srv_port = port

View File

@ -65,11 +65,11 @@ class AdafruitIoBackend(Backend):
def on_message(self, msg):
# noinspection PyUnusedLocal
def _handler(client, feed, data):
# noinspection PyBroadException
try:
data = float(data)
except:
pass
except Exception as e:
self.logger.debug('Not a number: {}: {}'.format(data, e))
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
return _handler

View File

@ -55,16 +55,17 @@ class Alarm:
self._runtime_snooze_interval = snooze_interval
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:
cron = croniter.croniter(self.when, now)
return cron.get_next()
except (AttributeError, croniter.CroniterBadCronError):
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):
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)))
return timestamp.timestamp() if timestamp >= now else None

View File

@ -1,6 +1,9 @@
from typing import Dict, Optional
import time
from threading import Thread, RLock
from typing import Dict, Optional, List
from platypush.backend.sensor import SensorBackend
from platypush.context import get_plugin
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
@ -20,10 +23,12 @@ class BluetoothScannerBackend(SensorBackend):
"""
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10, **kwargs):
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10,
track_devices: Optional[List[str]] = None, **kwargs):
"""
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
:param scan_duration: How long the scan should run (default: 10 seconds).
:param track_devices: List of addresses of devices to actively track, even if they aren't discoverable.
"""
super().__init__(plugin='bluetooth', plugin_args={
'device_id': device_id,
@ -31,17 +36,72 @@ class BluetoothScannerBackend(SensorBackend):
}, **kwargs)
self._last_seen_devices = {}
self._tracking_thread: Optional[Thread] = None
self._bt_lock = RLock()
self.track_devices = set(track_devices or [])
self.scan_duration = scan_duration
def _add_last_seen_device(self, dev):
addr = dev.pop('addr')
if addr not in self._last_seen_devices:
self.bus.post(BluetoothDeviceFoundEvent(address=addr, **dev))
self._last_seen_devices[addr] = {'addr': addr, **dev}
def _remove_last_seen_device(self, addr: str):
dev = self._last_seen_devices.get(addr)
if not dev:
return
self.bus.post(BluetoothDeviceLostEvent(address=addr, **dev))
del self._last_seen_devices[addr]
def _addr_tracker(self, addr):
with self._bt_lock:
name = get_plugin('bluetooth').lookup_name(addr, timeout=self.scan_duration).name
if name is None:
self._remove_last_seen_device(addr)
else:
self._add_last_seen_device({'addr': addr, 'name': name})
def _bt_tracker(self):
self.logger.info('Starting Bluetooth tracker')
while not self.should_stop():
trackers = []
for addr in self.track_devices:
tracker = Thread(target=self._addr_tracker, args=(addr,))
tracker.start()
trackers.append(tracker)
for tracker in trackers:
tracker.join(timeout=self.scan_duration)
time.sleep(self.scan_duration)
self.logger.info('Bluetooth tracker stopped')
def get_measurement(self):
with self._bt_lock:
return super().get_measurement()
def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]):
for addr, dev in data.items():
if addr not in self._last_seen_devices:
self.bus.post(BluetoothDeviceFoundEvent(address=dev.pop('addr'), **dev))
self._last_seen_devices[addr] = {'addr': addr, **dev}
self._add_last_seen_device(dev)
for addr, dev in self._last_seen_devices.copy().items():
if addr not in data:
self.bus.post(BluetoothDeviceLostEvent(address=dev.pop('addr'), **dev))
del self._last_seen_devices[addr]
if addr not in data and addr not in self.track_devices:
self._remove_last_seen_device(addr)
def run(self):
self._tracking_thread = Thread(target=self._bt_tracker)
self._tracking_thread.start()
super().run()
def on_stop(self):
super().on_stop()
if self._tracking_thread and self._tracking_thread.is_alive():
self.logger.info('Waiting for the Bluetooth tracking thread to stop')
self._tracking_thread.join(timeout=self.scan_duration)
# vim:sw=4:ts=4:et:

View File

@ -13,37 +13,41 @@ Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
import asyncio
from enum import Enum
from collections import namedtuple
import time
import struct
import itertools
class CreateConnectionChannelError(Enum):
NoError = 0
MaxPendingConnectionsReached = 1
class ConnectionStatus(Enum):
Disconnected = 0
Connected = 1
Ready = 2
class DisconnectReason(Enum):
Unspecified = 0
ConnectionEstablishmentFailed = 1
TimedOut = 2
BondingKeysMismatch = 3
class RemovedReason(Enum):
RemovedByThisClient = 0
ForceDisconnectedByThisClient = 1
ForceDisconnectedByOtherClient = 2
ButtonIsPrivate = 3
VerifyTimeout = 4
InternetBackendError = 5
InvalidData = 6
CouldntLoadDevice = 7
class ClickType(Enum):
ButtonDown = 0
ButtonUp = 1
@ -52,20 +56,24 @@ class ClickType(Enum):
ButtonDoubleClick = 4
ButtonHold = 5
class BdAddrType(Enum):
PublicBdAddrType = 0
RandomBdAddrType = 1
class LatencyMode(Enum):
NormalLatency = 0
LowLatency = 1
HighLatency = 2
class BluetoothControllerState(Enum):
Detached = 0
Resetting = 1
Attached = 2
class ScanWizardResult(Enum):
WizardSuccess = 0
WizardCancelledByUser = 1
@ -75,24 +83,26 @@ class ScanWizardResult(Enum):
WizardInternetBackendError = 5
WizardInvalidData = 6
class ButtonScanner:
"""ButtonScanner class.
Usage:
scanner = ButtonScanner()
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
client.add_scanner(scanner)
"""
_cnt = itertools.count()
def __init__(self):
self._scan_id = next(ButtonScanner._cnt)
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
class ScanWizard:
"""ScanWizard class
Usage:
wizard = ScanWizard()
wizard.on_found_private_button = lambda scan_wizard: ...
@ -101,9 +111,9 @@ class ScanWizard:
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
client.add_scan_wizard(wizard)
"""
_cnt = itertools.count()
def __init__(self):
self._scan_wizard_id = next(ScanWizard._cnt)
self._bd_addr = None
@ -113,33 +123,34 @@ class ScanWizard:
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
class ButtonConnectionChannel:
"""ButtonConnectionChannel class.
This class represents a connection channel to a Flic button.
Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel).
You may only have this connection channel added to one FlicClient at a time.
Before you add the connection channel to the client, you should set up your callback functions by assigning
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
referencing this object.
Available properties and the function parameters are:
on_create_connection_channel_response: channel, error, connection_status
on_removed: channel, removed_reason
on_connection_status_changed: channel, connection_status, disconnect_reason
on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff
"""
_cnt = itertools.count()
def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511):
def __init__(self, bd_addr, latency_mode=LatencyMode.NormalLatency, auto_disconnect_time=511):
self._conn_id = next(ButtonConnectionChannel._cnt)
self._bd_addr = bd_addr
self._latency_mode = latency_mode
self._auto_disconnect_time = auto_disconnect_time
self._client = None
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
self.on_removed = lambda channel, removed_reason: None
self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None
@ -147,61 +158,66 @@ class ButtonConnectionChannel:
self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
@property
def bd_addr(self):
return self._bd_addr
@property
def latency_mode(self):
return self._latency_mode
@latency_mode.setter
def latency_mode(self, latency_mode):
if self._client is None:
self._latency_mode = latency_mode
return
self._latency_mode = latency_mode
if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
self._client._send_command("CmdChangeModeParameters",
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
"auto_disconnect_time": self._auto_disconnect_time})
@property
def auto_disconnect_time(self):
return self._auto_disconnect_time
@auto_disconnect_time.setter
def auto_disconnect_time(self, auto_disconnect_time):
if self._client is None:
self._auto_disconnect_time = auto_disconnect_time
return
self._auto_disconnect_time = auto_disconnect_time
if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
self._client._send_command("CmdChangeModeParameters",
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
"auto_disconnect_time": self._auto_disconnect_time})
class FlicClient(asyncio.Protocol):
"""FlicClient class.
When this class is constructed, a socket connection is established.
You may then send commands to the server and set timers.
Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed.
For a more detailed description of all commands, events and enums, check the protocol specification.
All commands are wrapped in more high level functions and events are reported using callback functions.
All methods called on this class will take effect only if you eventually call the handle_events() method.
The ButtonScanner is used to set up a handler for advertisement packets.
The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events.
Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters):
on_new_verified_button: bd_addr
on_no_space_for_new_connection: max_concurrently_connected_buttons
on_got_space_for_new_connection: max_concurrently_connected_buttons
on_bluetooth_controller_state_change: state
"""
_EVENTS = [
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
@ -212,7 +228,8 @@ class FlicClient(asyncio.Protocol):
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtNewVerifiedButton", "<6s", "bd_addr"),
("EvtGetInfoResponse", "<B6sBBhBBH", "bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
("EvtGetInfoResponse", "<B6sBBhBBH",
"bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtBluetoothControllerStateChange", "<B", "state"),
@ -223,9 +240,9 @@ class FlicClient(asyncio.Protocol):
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
]
_EVENT_STRUCTS = list(map(lambda x: None if x == 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_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [
("CmdGetInfo", "", ""),
("CmdCreateScanner", "<I", "scan_id"),
@ -239,168 +256,169 @@ class FlicClient(asyncio.Protocol):
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
("CmdCancelScanWizard", "<I", "scan_wizard_id")
]
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
@staticmethod
def _bdaddr_bytes_to_string(bdaddr_bytes):
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
@staticmethod
def _bdaddr_string_to_bytes(bdaddr_string):
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
def __init__(self, loop,parent=None):
def __init__(self, loop, parent=None):
self.loop = loop
self.buffer=b""
self.transport=None
self.parent=parent
self.buffer = b""
self.transport = None
self.parent = parent
self._scanners = {}
self._scan_wizards = {}
self._connection_channels = {}
self._closed = False
self.on_new_verified_button = lambda bd_addr: None
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
self.on_bluetooth_controller_state_change = lambda state: None
self.on_bluetooth_controller_state_change = lambda state: None
self.on_get_info = lambda items: None
self.on_get_button_uuid = lambda addr, uuid: None
def connection_made(self, transport):
self.transport=transport
self.transport = transport
if self.parent:
self.parent.register_protocol(self)
def close(self):
"""Closes the client. The handle_events() method will return."""
if self._closed:
return
self._closed = True
def add_scanner(self, scanner):
"""Add a ButtonScanner object.
The scan will start directly once the scanner is added.
"""
if scanner._scan_id in self._scanners:
return
self._scanners[scanner._scan_id] = scanner
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
def remove_scanner(self, scanner):
"""Remove a ButtonScanner object.
You will no longer receive advertisement packets.
"""
if scanner._scan_id not in self._scanners:
return
del self._scanners[scanner._scan_id]
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
def add_scan_wizard(self, scan_wizard):
"""Add a ScanWizard object.
The scan wizard will start directly once the scan wizard is added.
"""
if scan_wizard._scan_wizard_id in self._scan_wizards:
return
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
def cancel_scan_wizard(self, scan_wizard):
"""Cancel a ScanWizard.
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
"""
if scan_wizard._scan_wizard_id not in self._scan_wizards:
return
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
def add_connection_channel(self, channel):
"""Adds a connection channel to a specific Flic button.
This will start listening for a specific Flic button's connection and button events.
Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
The on_create_connection_channel_response callback property will be called on the
connection channel after this command has been received by the server.
You may have as many connection channels as you wish for a specific Flic Button.
"""
if channel._conn_id in self._connection_channels:
return
channel._client = self
self._connection_channels[channel._conn_id] = channel
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr,
"latency_mode": channel._latency_mode,
"auto_disconnect_time": channel._auto_disconnect_time})
def remove_connection_channel(self, channel):
"""Remove a connection channel.
This will stop listening for new events for a specific connection channel that has previously been added.
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
"""
if channel._conn_id not in self._connection_channels:
return
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
def force_disconnect(self, bd_addr):
"""Force disconnection or cancel pending connection of a specific Flic button.
This removes all connection channels for all clients connected to the server for this specific Flic button.
"""
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
def get_info(self):
"""Get info about the current state of the server.
The server will send back its information directly and the callback will be called once the response arrives.
The callback takes only one parameter: info. This info parameter is a dictionary with the following objects:
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
"""
self._send_command("CmdGetInfo", {})
def get_button_uuid(self, bd_addr):
"""Get button uuid for a verified button.
The server will send back its information directly and the callback will be called once the response arrives.
Responses will arrive in the same order as requested.
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
"""
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
def run_on_handle_events_thread(self, callback):
"""Run a function on the thread that handles the events."""
if threading.get_ident() == self._handle_event_thread_ident:
callback()
else:
self.set_timer(0, callback)
def _send_command(self, name, items):
for key, value in items.items():
if isinstance(value, Enum):
items[key] = value.value
if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes()
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
bytes = bytearray(3)
@ -409,83 +427,85 @@ class FlicClient(asyncio.Protocol):
bytes[2] = opcode
bytes += data_bytes
self.transport.write(bytes)
def _dispatch_event(self, data):
if len(data) == 0:
return
opcode = data[0]
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None:
return
event_name = FlicClient._EVENTS[opcode][0]
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size])
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1: 1 + FlicClient._EVENT_STRUCTS[opcode].size])
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
# Process some kind of items whose data type is not supported by struct
if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string()
if "name" in items:
items["name"] = items["name"].decode("utf-8")
if event_name == "EvtCreateConnectionChannelResponse":
items["error"] = CreateConnectionChannelError(items["error"])
items["connection_status"] = ConnectionStatus(items["connection_status"])
if event_name == "EvtConnectionStatusChanged":
items["connection_status"] = ConnectionStatus(items["connection_status"])
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
if event_name == "EvtConnectionChannelRemoved":
items["removed_reason"] = RemovedReason(items["removed_reason"])
if event_name.startswith("EvtButton"):
items["click_type"] = ClickType(items["click_type"])
if event_name == "EvtGetInfoResponse":
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string()
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
items["bd_addr_of_verified_buttons"] = []
pos = FlicClient._EVENT_STRUCTS[opcode].size
for i in range(items["nb_verified_buttons"]):
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
items["bd_addr_of_verified_buttons"].append(
FlicClient._bdaddr_bytes_to_string())
pos += 6
if event_name == "EvtBluetoothControllerStateChange":
items["state"] = BluetoothControllerState(items["state"])
if event_name == "EvtGetButtonUUIDResponse":
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
if items["uuid"] == "00000000000000000000000000000000":
items["uuid"] = None
if event_name == "EvtScanWizardCompleted":
items["result"] = ScanWizardResult(items["result"])
# Process event
if event_name == "EvtAdvertisementPacket":
scanner = self._scanners.get(items["scan_id"])
if scanner is not None:
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"],
items["is_private"], items["already_verified"])
if event_name == "EvtCreateConnectionChannelResponse":
channel = self._connection_channels[items["conn_id"]]
if items["error"] != CreateConnectionChannelError.NoError:
del self._connection_channels[items["conn_id"]]
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
if event_name == "EvtConnectionStatusChanged":
channel = self._connection_channels[items["conn_id"]]
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
if event_name == "EvtConnectionChannelRemoved":
channel = self._connection_channels[items["conn_id"]]
del self._connection_channels[items["conn_id"]]
channel.on_removed(channel, items["removed_reason"])
if event_name == "EvtButtonUpOrDown":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
@ -494,61 +514,60 @@ class FlicClient(asyncio.Protocol):
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtButtonSingleOrDoubleClick":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"])
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"],
items["time_diff"])
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"],
items["time_diff"])
if event_name == "EvtNewVerifiedButton":
self.on_new_verified_button(items["bd_addr"])
if event_name == "EvtGetInfoResponse":
self.on_get_info(items)
if event_name == "EvtNoSpaceForNewConnection":
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
if event_name == "EvtGotSpaceForNewConnection":
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
if event_name == "EvtBluetoothControllerStateChange":
self.on_bluetooth_controller_state_change(items["state"])
if event_name == "EvtGetButtonUUIDResponse":
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
if event_name == "EvtScanWizardFoundPrivateButton":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_found_private_button(scan_wizard)
if event_name == "EvtScanWizardFoundPublicButton":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard._bd_addr = items["bd_addr"]
scan_wizard._name = items["name"]
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
if event_name == "EvtScanWizardButtonConnected":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
if event_name == "EvtScanWizardCompleted":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
del self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
def data_received(self,data):
cdata=self.buffer+data
self.buffer=b""
def data_received(self, data):
cdata = self.buffer + data
self.buffer = b""
while len(cdata):
packet_len = cdata[0] | (cdata[1] << 8)
packet_len += 2
if len(cdata)>= packet_len:
if len(cdata) >= packet_len:
self._dispatch_event(cdata[2:packet_len])
cdata=cdata[packet_len:]
cdata = cdata[packet_len:]
else:
if len(cdata):
self.buffer=cdata #unlikely to happen but.....
self.buffer = cdata # unlikely to happen but.....
break

View File

@ -40,12 +40,12 @@ class RemovedReason(Enum):
RemovedByThisClient = 0
ForceDisconnectedByThisClient = 1
ForceDisconnectedByOtherClient = 2
ButtonIsPrivate = 3
VerifyTimeout = 4
InternetBackendError = 5
InvalidData = 6
CouldntLoadDevice = 7
class ClickType(Enum):
@ -81,22 +81,22 @@ class ScanWizardResult(Enum):
class ButtonScanner:
"""ButtonScanner class.
Usage:
scanner = ButtonScanner()
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
client.add_scanner(scanner)
"""
_cnt = itertools.count()
def __init__(self):
self._scan_id = next(ButtonScanner._cnt)
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
class ScanWizard:
"""ScanWizard class
Usage:
wizard = ScanWizard()
wizard.on_found_private_button = lambda scan_wizard: ...
@ -105,9 +105,9 @@ class ScanWizard:
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
client.add_scan_wizard(wizard)
"""
_cnt = itertools.count()
def __init__(self):
self._scan_wizard_id = next(ScanWizard._cnt)
self._bd_addr = None
@ -119,31 +119,31 @@ class ScanWizard:
class ButtonConnectionChannel:
"""ButtonConnectionChannel class.
This class represents a connection channel to a Flic button.
Add this button connection channel to a FlicClient by executing client.add_connection_channel(connection_channel).
You may only have this connection channel added to one FlicClient at a time.
Before you add the connection channel to the client, you should set up your callback functions by assigning
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
referencing this object.
Available properties and the function parameters are:
on_create_connection_channel_response: channel, error, connection_status
on_removed: channel, removed_reason
on_connection_status_changed: channel, connection_status, disconnect_reason
on_button_up_or_down / on_button_click_or_hold / on_button_single_or_double_click / on_button_single_or_double_click_or_hold: channel, click_type, was_queued, time_diff
"""
_cnt = itertools.count()
def __init__(self, bd_addr, latency_mode = LatencyMode.NormalLatency, auto_disconnect_time = 511):
self._conn_id = next(ButtonConnectionChannel._cnt)
self._bd_addr = bd_addr
self._latency_mode = latency_mode
self._auto_disconnect_time = auto_disconnect_time
self._client = None
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
self.on_removed = lambda channel, removed_reason: None
self.on_connection_status_changed = lambda channel, connection_status, disconnect_reason: None
@ -151,36 +151,36 @@ class ButtonConnectionChannel:
self.on_button_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
self.on_button_single_or_double_click = lambda channel, click_type, was_queued, time_diff: None
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
@property
def bd_addr(self):
return self._bd_addr
@property
def latency_mode(self):
return self._latency_mode
@latency_mode.setter
def latency_mode(self, latency_mode):
if self._client is None:
self._latency_mode = latency_mode
return
with self._client._lock:
self._latency_mode = latency_mode
if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
@property
def auto_disconnect_time(self):
return self._auto_disconnect_time
@auto_disconnect_time.setter
def auto_disconnect_time(self, auto_disconnect_time):
if self._client is None:
self._auto_disconnect_time = auto_disconnect_time
return
with self._client._lock:
self._auto_disconnect_time = auto_disconnect_time
if not self._client._closed:
@ -188,26 +188,26 @@ class ButtonConnectionChannel:
class FlicClient:
"""FlicClient class.
When this class is constructed, a socket connection is established.
You may then send commands to the server and set timers.
Once you are ready with the initialization you must call the handle_events() method which is a main loop that never exits, unless the socket is closed.
For a more detailed description of all commands, events and enums, check the protocol specification.
All commands are wrapped in more high level functions and events are reported using callback functions.
All methods called on this class will take effect only if you eventually call the handle_events() method.
The ButtonScanner is used to set up a handler for advertisement packets.
The ButtonConnectionChannel is used to interact with connections to flic buttons and receive their events.
Other events are handled by the following callback functions that can be assigned to this object (and a list of the callback function parameters):
on_new_verified_button: bd_addr
on_no_space_for_new_connection: max_concurrently_connected_buttons
on_got_space_for_new_connection: max_concurrently_connected_buttons
on_bluetooth_controller_state_change: state
"""
_EVENTS = [
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
@ -229,9 +229,9 @@ class FlicClient:
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
]
_EVENT_STRUCTS = list(map(lambda x: None if x == 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_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [
("CmdGetInfo", "", ""),
("CmdCreateScanner", "<I", "scan_id"),
@ -245,17 +245,19 @@ class FlicClient:
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
("CmdCancelScanWizard", "<I", "scan_wizard_id")
]
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
@staticmethod
def _bdaddr_bytes_to_string(bdaddr_bytes):
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
@staticmethod
def _bdaddr_string_to_bytes(bdaddr_string):
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
def __init__(self, host, port = 5551):
self._sock = socket.create_connection((host, port), None)
self._lock = threading.RLock()
@ -267,113 +269,113 @@ class FlicClient:
self._timers = queue.PriorityQueue()
self._handle_event_thread_ident = None
self._closed = False
self.on_new_verified_button = lambda bd_addr: None
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: None
self.on_got_space_for_new_connection = lambda max_concurrently_connected_buttons: None
self.on_bluetooth_controller_state_change = lambda state: None
def close(self):
"""Closes the client. The handle_events() method will return."""
with self._lock:
if self._closed:
return
if threading.get_ident() != self._handle_event_thread_ident:
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
self._closed = True
def add_scanner(self, scanner):
"""Add a ButtonScanner object.
The scan will start directly once the scanner is added.
"""
with self._lock:
if scanner._scan_id in self._scanners:
return
self._scanners[scanner._scan_id] = scanner
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
def remove_scanner(self, scanner):
"""Remove a ButtonScanner object.
You will no longer receive advertisement packets.
"""
with self._lock:
if scanner._scan_id not in self._scanners:
return
del self._scanners[scanner._scan_id]
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
def add_scan_wizard(self, scan_wizard):
"""Add a ScanWizard object.
The scan wizard will start directly once the scan wizard is added.
"""
with self._lock:
if scan_wizard._scan_wizard_id in self._scan_wizards:
return
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
def cancel_scan_wizard(self, scan_wizard):
"""Cancel a ScanWizard.
Note: The effect of this command will take place at the time the on_completed event arrives on the scan wizard object.
If cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
"""
with self._lock:
if scan_wizard._scan_wizard_id not in self._scan_wizards:
return
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
def add_connection_channel(self, channel):
"""Adds a connection channel to a specific Flic button.
This will start listening for a specific Flic button's connection and button events.
Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
The on_create_connection_channel_response callback property will be called on the
connection channel after this command has been received by the server.
You may have as many connection channels as you wish for a specific Flic Button.
"""
with self._lock:
if channel._conn_id in self._connection_channels:
return
channel._client = self
self._connection_channels[channel._conn_id] = channel
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
def remove_connection_channel(self, channel):
"""Remove a connection channel.
This will stop listening for new events for a specific connection channel that has previously been added.
Note: The effect of this command will take place at the time the on_removed event arrives on the connection channel object.
"""
with self._lock:
if channel._conn_id not in self._connection_channels:
return
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
def force_disconnect(self, bd_addr):
"""Force disconnection or cancel pending connection of a specific Flic button.
This removes all connection channels for all clients connected to the server for this specific Flic button.
"""
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
def get_info(self, callback):
"""Get info about the current state of the server.
The server will send back its information directly and the callback will be called once the response arrives.
The callback takes only one parameter: info. This info parameter is a dictionary with the following objects:
bluetooth_controller_state, my_bd_addr, my_bd_addr_type, max_pending_connections, max_concurrently_connected_buttons,
@ -381,47 +383,47 @@ class FlicClient:
"""
self._get_info_response_queue.put(callback)
self._send_command("CmdGetInfo", {})
def get_button_uuid(self, bd_addr, callback):
"""Get button uuid for a verified button.
The server will send back its information directly and the callback will be called once the response arrives.
Responses will arrive in the same order as requested.
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
"""
with self._lock:
self._get_button_uuid_queue.put(callback)
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
def set_timer(self, timeout_millis, callback):
"""Set a timer
This timer callback will run after the specified timeout_millis on the thread that handles the events.
"""
point_in_time = time.monotonic() + timeout_millis / 1000.0
self._timers.put((point_in_time, callback))
if threading.get_ident() != self._handle_event_thread_ident:
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
def run_on_handle_events_thread(self, callback):
"""Run a function on the thread that handles the events."""
if threading.get_ident() == self._handle_event_thread_ident:
callback()
else:
self.set_timer(0, callback)
def _send_command(self, name, items):
for key, value in items.items():
if isinstance(value, Enum):
items[key] = value.value
if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
bytes = bytearray(3)
@ -432,83 +434,83 @@ class FlicClient:
with self._lock:
if not self._closed:
self._sock.sendall(bytes)
def _dispatch_event(self, data):
if len(data) == 0:
return
opcode = data[0]
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None:
return
event_name = FlicClient._EVENTS[opcode][0]
data_tuple = FlicClient._EVENT_STRUCTS[opcode].unpack(data[1 : 1 + FlicClient._EVENT_STRUCTS[opcode].size])
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
# Process some kind of items whose data type is not supported by struct
if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
if "name" in items:
items["name"] = items["name"].decode("utf-8")
if event_name == "EvtCreateConnectionChannelResponse":
items["error"] = CreateConnectionChannelError(items["error"])
items["connection_status"] = ConnectionStatus(items["connection_status"])
if event_name == "EvtConnectionStatusChanged":
items["connection_status"] = ConnectionStatus(items["connection_status"])
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
if event_name == "EvtConnectionChannelRemoved":
items["removed_reason"] = RemovedReason(items["removed_reason"])
if event_name.startswith("EvtButton"):
items["click_type"] = ClickType(items["click_type"])
if event_name == "EvtGetInfoResponse":
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
items["bd_addr_of_verified_buttons"] = []
pos = FlicClient._EVENT_STRUCTS[opcode].size
for i in range(items["nb_verified_buttons"]):
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
pos += 6
if event_name == "EvtBluetoothControllerStateChange":
items["state"] = BluetoothControllerState(items["state"])
if event_name == "EvtGetButtonUUIDResponse":
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
if items["uuid"] == "00000000000000000000000000000000":
items["uuid"] = None
if event_name == "EvtScanWizardCompleted":
items["result"] = ScanWizardResult(items["result"])
# Process event
if event_name == "EvtAdvertisementPacket":
scanner = self._scanners.get(items["scan_id"])
if scanner is not None:
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
if event_name == "EvtCreateConnectionChannelResponse":
channel = self._connection_channels[items["conn_id"]]
if items["error"] != CreateConnectionChannelError.NoError:
del self._connection_channels[items["conn_id"]]
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
if event_name == "EvtConnectionStatusChanged":
channel = self._connection_channels[items["conn_id"]]
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
if event_name == "EvtConnectionChannelRemoved":
channel = self._connection_channels[items["conn_id"]]
del self._connection_channels[items["conn_id"]]
channel.on_removed(channel, items["removed_reason"])
if event_name == "EvtButtonUpOrDown":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
@ -521,44 +523,44 @@ class FlicClient:
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtNewVerifiedButton":
self.on_new_verified_button(items["bd_addr"])
if event_name == "EvtGetInfoResponse":
self._get_info_response_queue.get()(items)
if event_name == "EvtNoSpaceForNewConnection":
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
if event_name == "EvtGotSpaceForNewConnection":
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
if event_name == "EvtBluetoothControllerStateChange":
self.on_bluetooth_controller_state_change(items["state"])
if event_name == "EvtGetButtonUUIDResponse":
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
if event_name == "EvtScanWizardFoundPrivateButton":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_found_private_button(scan_wizard)
if event_name == "EvtScanWizardFoundPublicButton":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard._bd_addr = items["bd_addr"]
scan_wizard._name = items["name"]
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
if event_name == "EvtScanWizardButtonConnected":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
if event_name == "EvtScanWizardCompleted":
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
del self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
def _handle_one_event(self):
if len(self._timers.queue) > 0:
current_timer = self._timers.queue[0]
@ -568,10 +570,10 @@ class FlicClient:
return True
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
return True
len_arr = bytearray(2)
view = memoryview(len_arr)
toread = 2
while toread > 0:
nbytes = self._sock.recv_into(view, toread)
@ -579,7 +581,7 @@ class FlicClient:
return False
view = view[nbytes:]
toread -= nbytes
packet_len = len_arr[0] | (len_arr[1] << 8)
data = bytearray(packet_len)
view = memoryview(data)
@ -590,13 +592,13 @@ class FlicClient:
return False
view = view[nbytes:]
toread -= nbytes
self._dispatch_event(data)
return True
def handle_events(self):
"""Start the main loop for this client.
This method will not return until the socket has been closed.
Once it has returned, any use of this FlicClient is illegal.
"""

View File

@ -34,7 +34,7 @@ class CameraPiBackend(Backend):
return self.value == other
# 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',
start_recording_on_startup=True,
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
:type listen_port: int
:param bind_address: Bind address (default: 0.0.0.0).
:type bind_address: str
"""
super().__init__(**kwargs)
self.bind_address = bind_address
self.listen_port = listen_port
self.server_socket = socket.socket()
self.server_socket.bind(('0.0.0.0', self.listen_port))
self.server_socket.bind((self.bind_address, self.listen_port))
self.server_socket.listen(0)
import picamera
@ -134,13 +138,13 @@ class CameraPiBackend(Backend):
self.logger.info('Client closed connection')
try:
self.stop_recording()
except:
pass
except Exception as e:
self.logger.warning('Could not stop recording: {}'.format(str(e)))
try:
connection.close()
except:
pass
except Exception as e:
self.logger.warning('Could not close connection: {}'.format(str(e)))
self.send_camera_action(self.CameraAction.START_RECORDING)

View File

@ -0,0 +1,128 @@
from typing import Iterable, Dict, Union, Any
from watchdog.observers import Observer
from platypush.backend import Backend
from .entities.handlers import EventHandler
from .entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
class FileMonitorBackend(Backend):
"""
This backend monitors changes to local files and directories using the Watchdog API.
Triggers:
* :class:`platypush.message.event.file.FileSystemCreateEvent` if a resource is created.
* :class:`platypush.message.event.file.FileSystemDeleteEvent` if a resource is removed.
* :class:`platypush.message.event.file.FileSystemModifyEvent` if a resource is modified.
Requires:
* **watchdog** (``pip install watchdog``)
"""
class EventHandlerFactory:
"""
Create a file system event handler from a string, dictionary or ``MonitoredResource`` resource.
"""
@staticmethod
def from_resource(resource: Union[str, Dict[str, Any], MonitoredResource]) -> EventHandler:
if isinstance(resource, str):
resource = MonitoredResource(resource)
elif isinstance(resource, dict):
if 'patterns' in resource or 'ignore_patterns' in resource:
resource = MonitoredPattern(**resource)
elif 'regexes' in resource or 'ignore_regexes' in resource:
resource = MonitoredRegex(**resource)
else:
resource = MonitoredResource(**resource)
return EventHandler.from_resource(resource)
def __init__(self, paths: Iterable[Union[str, Dict[str, Any], MonitoredResource]], **kwargs):
"""
:param paths: List of paths to monitor. Paths can either be expressed in any of the following ways:
- Simple strings. In this case, paths will be interpreted as absolute references to a file or a directory
to monitor. Example:
.. code-block:: yaml
backend.file.monitor:
paths:
# Monitor changes on the /tmp folder
- /tmp
# Monitor changes on /etc/passwd
- /etc/passwd
- Path with monitoring properties expressed as a key-value object. Example showing the supported attributes:
.. code-block:: yaml
backend.file.monitor:
paths:
# Monitor changes on the /tmp folder and its subfolders
- path: /tmp
recursive: True
- Path with pattern-based search criteria for the files to monitor and exclude. Example:
.. code-block:: yaml
backend.file.monitor:
paths:
# Recursively monitor changes on the ~/my-project folder that include all
# *.py files, excluding those whose name starts with tmp_ and
# all the files contained in the __pycache__ folders
- path: ~/my-project
recursive: True
patterns:
- "*.py"
ignore_patterns:
- "tmp_*"
ignore_directories:
- "__pycache__"
- Path with regex-based search criteria for the files to monitor and exclude. Example:
.. code-block:: yaml
backend.file.monitor:
paths:
# Recursively monitor changes on the ~/my-images folder that include all
# the files matching a JPEG extension in case-insensitive mode,
# excluding those whose name starts with tmp_ and
# all the files contained in the __MACOSX folders
- path: ~/my-images
recursive: True
case_sensitive: False
regexes:
- '.*\\.jpe?g$'
ignore_patterns:
- '^tmp_.*'
ignore_directories:
- '__MACOSX'
"""
super().__init__(**kwargs)
self._observer = Observer()
for path in paths:
handler = self.EventHandlerFactory.from_resource(path)
self._observer.schedule(handler, handler.resource.path, recursive=handler.resource.recursive)
def run(self):
super().run()
self.logger.info('Initializing file monitor backend')
self._observer.start()
self.wait_stop()
def on_stop(self):
self.logger.info('Stopping file monitor backend')
self._observer.stop()
self._observer.join()
self.logger.info('Stopped file monitor backend')

View File

@ -0,0 +1,61 @@
import os
from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler, RegexMatchingEventHandler
from platypush.backend.file.monitor.entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
from platypush.context import get_bus
from platypush.message.event.file import FileSystemModifyEvent, FileSystemCreateEvent, FileSystemDeleteEvent
class EventHandler(FileSystemEventHandler):
"""
Base class for Watchdog event handlers.
"""
def __init__(self, resource: MonitoredResource, **kwargs):
super().__init__(**kwargs)
resource.path = os.path.expanduser(resource.path)
self.resource = resource
def on_created(self, event):
get_bus().post(FileSystemCreateEvent(path=event.src_path, is_directory=event.is_directory))
def on_deleted(self, event):
get_bus().post(FileSystemDeleteEvent(path=event.src_path, is_directory=event.is_directory))
def on_modified(self, event):
get_bus().post(FileSystemModifyEvent(path=event.src_path, is_directory=event.is_directory))
def on_moved(self, event):
pass
@classmethod
def from_resource(cls, resource: MonitoredResource):
if isinstance(resource, MonitoredPattern):
return PatternEventHandler(resource)
if isinstance(resource, MonitoredRegex):
return RegexEventHandler(resource)
return cls(resource)
class PatternEventHandler(EventHandler, PatternMatchingEventHandler):
"""
Event handler for file patterns.
"""
def __init__(self, resource: MonitoredPattern):
super().__init__(resource=resource,
patterns=resource.patterns,
ignore_patterns=resource.ignore_patterns,
ignore_directories=resource.ignore_directories,
case_sensitive=resource.case_sensitive)
class RegexEventHandler(EventHandler, RegexMatchingEventHandler):
"""
Event handler for regex-based file patterns.
"""
def __init__(self, resource: MonitoredRegex):
super().__init__(resource=resource,
regexes=resource.regexes,
ignore_regexes=resource.ignore_regexes,
ignore_directories=resource.ignore_directories,
case_sensitive=resource.case_sensitive)

View File

@ -0,0 +1,24 @@
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class MonitoredResource:
path: str
recursive: bool = False
@dataclass
class MonitoredPattern(MonitoredResource):
patterns: Optional[List[str]] = None
ignore_patterns: Optional[List[str]] = None
ignore_directories: Optional[List[str]] = None
case_sensitive: bool = True
@dataclass
class MonitoredRegex(MonitoredResource):
regexes: Optional[List[str]] = None
ignore_regexes: Optional[List[str]] = None
ignore_directories: Optional[List[str]] = None
case_sensitive: bool = True

View File

@ -51,11 +51,10 @@ class GooglePubsubBackend(Backend):
def _message_callback(self, topic):
def callback(msg):
data = msg.data.decode()
# noinspection PyBroadException
try:
data = json.loads(data)
except:
pass
except Exception as e:
self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e)))
msg.ack()
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))

View File

@ -391,6 +391,7 @@ class HttpBackend(Backend):
'debug': False,
}
application.config['redis_queue'] = self.bus.redis_queue
if self.ssl_context:
kwargs['ssl_context'] = self.ssl_context

View File

@ -28,7 +28,8 @@ base_folder = os.path.abspath(os.path.join(
template_folder = os.path.join(base_folder, 'webapp/dist')
static_folder = os.path.join(base_folder, 'webapp/dist/static')
application = Flask('platypush', template_folder=template_folder,
application = Flask('platypush',
template_folder=template_folder,
static_folder=static_folder)
for route in get_routes():

View File

@ -31,8 +31,8 @@ def _hook(hook_name):
# noinspection PyBroadException
try:
event_args['data'] = json.loads(event_args['data'])
except:
pass
except Exception as e:
logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e)))
event = WebhookEvent(**event_args)

View File

@ -31,7 +31,7 @@ def login():
if session_token:
user, session = user_manager.authenticate_user_session(session_token)
if user:
return redirect(redirect_page, 302)
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
if request.form:
username = request.form.get('username')
@ -44,7 +44,7 @@ def login():
expires_at=expires)
if session:
redirect_target = redirect(redirect_page, 302)
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
response = make_response(redirect_target)
response.set_cookie('session_token', session.session_token, expires=expires)
return response

View File

@ -25,7 +25,7 @@ def logout():
if not user:
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.set_cookie('session_token', '', expires=0)
return response

View File

@ -45,14 +45,12 @@ def get_args(kwargs):
if k == 'resolution':
v = json.loads('[{}]'.format(v))
else:
# noinspection PyBroadException
try:
v = int(v)
except:
# noinspection PyBroadException
except (ValueError, TypeError):
try:
v = float(v)
except:
except (ValueError, TypeError):
pass
kwargs[k] = v

View File

@ -32,8 +32,8 @@ def add_media():
args = {}
try:
args = json.loads(request.data.decode('utf-8'))
except:
abort(400, 'Invalid JSON request')
except Exception as e:
abort(400, 'Invalid JSON request: {}'.format(str(e)))
source = args.get('source')
if not source:

View File

@ -40,7 +40,7 @@ def audio_feed(device, fifo, sample_rate, blocksize, latency, channels):
channels=channels)
try:
with open(fifo, 'rb') as f:
with open(fifo, 'rb') as f: # lgtm [py/path-injection]
send_header = True
while True:

View File

@ -31,10 +31,10 @@ def register():
if session_token:
user, session = user_manager.authenticate_user_session(session_token)
if user:
return redirect(redirect_page, 302)
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
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:
username = request.form.get('username')
@ -49,7 +49,7 @@ def register():
if not remember else None)
if session:
redirect_target = redirect(redirect_page, 302)
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
response = make_response(redirect_target)
response.set_cookie('session_token', session.session_token)
return response

View File

@ -3,7 +3,7 @@ import logging
import os
from functools import wraps
from flask import abort, request, redirect, Response
from flask import abort, request, redirect, Response, current_app
from redis import Redis
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default
@ -23,7 +23,7 @@ _logger = None
def bus():
global _bus
if _bus is None:
_bus = RedisBus()
_bus = RedisBus(redis_queue=current_app.config.get('redis_queue'))
return _bus
@ -123,7 +123,8 @@ def _authenticate_token():
try:
user_manager.validate_jwt_token(user_token)
return True
except:
except Exception as e:
logger().debug(str(e))
return token and user_token == token

View File

@ -78,11 +78,11 @@ class HttpRequest(object):
def get_new_items(self, 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):
for (key, value) in self.request_args.items():
yield (key, value)
yield key, value
class JsonHttpRequest(HttpRequest):
@ -96,7 +96,7 @@ class JsonHttpRequest(HttpRequest):
new_entries = []
if self.path:
m = re.match('\$\{\s*(.*)\s*\}', self.path)
m = re.match(r'\${\s*(.*)\s*}', self.path)
response = eval(m.group(1))
for entry in response:

View File

@ -238,15 +238,15 @@ class RssUpdates(HttpRequest):
with open(digest_filename, 'w', encoding='utf-8') as f:
f.write(content)
elif self.digest_format == 'pdf':
import weasyprint
from weasyprint import HTML, CSS
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()
css = [weasyprint.CSS('https://fonts.googleapis.com/css?family=Merriweather'),
weasyprint.CSS(string=body_style, font_config=font_config)]
css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'),
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:
raise RuntimeError('Unsupported format: {}. Supported formats: ' +
'html or pdf'.format(self.digest_format))

View File

@ -50,15 +50,13 @@ class HttpUtils(object):
for name, resource_path in resource_dirs.items():
resource_path = os.path.abspath(os.path.expanduser(resource_path))
if directory.startswith(resource_path):
subdir = re.sub('^{}(.*)$'.format(resource_path),
'\\1', directory)
uri = '/resources/' + name
break
if not uri:
raise RuntimeError(('Directory {} not found among the available ' +
'static resources on the webserver').format(
directory))
'static resources on the webserver').format(
directory))
results = [
re.sub('^{}(.*)$'.format(resource_path), uri + '\\1', path)
@ -92,10 +90,11 @@ class HttpUtils(object):
@classmethod
def plugin_name_to_tag(cls, module_name):
return module_name.replace('.','-')
return module_name.replace('.', '-')
@classmethod
def find_templates_in_dir(cls, directory):
# noinspection PyTypeChecker
return [
os.path.join(directory, file)
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

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