Compare commits
464 commits
Author | SHA1 | Date | |
---|---|---|---|
db8ea33b68 | |||
03631bcebc | |||
ade3a7c2cf | |||
1f6c7aae60 | |||
a6c7d64511 | |||
af7977bcf7 | |||
0762004838 | |||
|
c8bfbae4f0 | ||
|
d35a9729a4 | ||
a39452124d | |||
fc1d9ad3e6 | |||
7ee869ce42 | |||
df36a9f811 | |||
abf793e703 | |||
132c659d3c | |||
acc4f1c0e3 | |||
d7d5bcdd0c | |||
def8c0dd76 | |||
6cc28a3c3b | |||
93c3327bcd | |||
85d975edc6 | |||
d767cafafe | |||
cee8f9f8e0 | |||
b2e2ae9538 | |||
f296f4b161 | |||
9eab526e47 | |||
8f6404d0b1 | |||
eac26b9b22 | |||
b42c491390 | |||
c7fb97cdc7 | |||
18e99c6f12 | |||
664ce4050d | |||
69583d2e15 | |||
2f840200be | |||
46aef7c8b5 | |||
5ca15937e3 | |||
ce882381c0 | |||
99b35b292f | |||
|
174439a8ed | ||
3a18e9faf4 | |||
f8d76fe4eb | |||
9fa3385766 | |||
|
4fe5322600 | ||
2224681e3c | |||
68c44c0c3c | |||
02a22d4a88 | |||
b9bc4b5fe0 | |||
c006c4b368 | |||
75e1f35523 | |||
9e08b731a5 | |||
|
edfa5ed16f | ||
|
f2628f4f2c | ||
f1faa1141e | |||
2a78f81a7b | |||
86761e7088 | |||
89beab4767 | |||
8db8f3e6c4 | |||
ee0685363e | |||
33276bf697 | |||
99831bf0c7 | |||
641d9c0d41 | |||
95625a401d | |||
a147a4d37a | |||
177c697f83 | |||
eb486df1ee | |||
a4c70f1e4d | |||
c16f8aa39e | |||
570f1d0cf6 | |||
|
4313b6e883 | ||
00fabf3853 | |||
cad184fc1f | |||
928bb3667a | |||
782be7794b | |||
40dc739d09 | |||
4821fe086b | |||
8d621b2688 | |||
1355f7a3f6 | |||
3ce98305f0 | |||
0a4cadba3e | |||
9e46ab0b60 | |||
c74d2fb124 | |||
ca573cb980 | |||
75deb0393d | |||
14f1c44378 | |||
fdd46edb6a | |||
b9738d88df | |||
f92d19a24e | |||
ce0ca2e9ee | |||
7f157d0234 | |||
8a3df30001 | |||
82274d3d12 | |||
ab6c85c2a6 | |||
f8564c19cd | |||
bf519babb0 | |||
632a7ab792 | |||
1d3d741212 | |||
b171cb1012 | |||
352d421e61 | |||
6f224cbda9 | |||
adb472da7f | |||
|
347a4d2555 | ||
cef310ffd7 | |||
96588df83b | |||
02f6845e72 | |||
40834f7ce5 | |||
3d6af00ee6 | |||
b06867dc5d | |||
22dad79dd5 | |||
211025cedb | |||
|
da27ed7546 | ||
775478fff0 | |||
093bac3a60 | |||
b3606a8ac3 | |||
4902475caf | |||
7687e52058 | |||
eae4b4f62a | |||
296458ece3 | |||
71af6e87e0 | |||
c659ec507f | |||
fb1953ce34 | |||
34a108bbcb | |||
5be6ca50f0 | |||
090e7d6de8 | |||
6f85318868 | |||
8f256e4077 | |||
4ed80a0945 | |||
ca90060ba1 | |||
49ad3261f1 | |||
73e16fa6b1 | |||
d860d8aef1 | |||
|
36aee6f787 | ||
|
04ff008800 | ||
|
47ba13d985 | ||
4ada1c663d | |||
210cefc1a4 | |||
e43147e6a3 | |||
66c1e59c61 | |||
0e3845ef88 | |||
833f810d4b | |||
d190560536 | |||
e0e3081eb1 | |||
708ce210ba | |||
d0707b6479 | |||
66445cb4e4 | |||
f93df2fd49 | |||
0d806eeb6e | |||
36fdcf6963 | |||
3932fb56c4 | |||
cde5e4e4f9 | |||
b4f9472fc5 | |||
9e00428568 | |||
b4258faa29 | |||
9e4daacd74 | |||
61c5bae527 | |||
82fcf86900 | |||
a5f02c6a30 | |||
076cc6a63e | |||
d0a579cf4b | |||
33af368940 | |||
531be19a66 | |||
ef36c76f10 | |||
571a8ca9d1 | |||
2800bac3fb | |||
d1b7b1768c | |||
1a7d0a3b07 | |||
|
b27c9ee630 | ||
04a23d555d | |||
dae8cf0111 | |||
67702d3cc8 | |||
6df336465f | |||
ffb7a3e5a3 | |||
151d0008ee | |||
10eb0c12aa | |||
aa5cbbce28 | |||
53fb254c57 | |||
727094467d | |||
|
6f9428487f | ||
3e777bd19f | |||
7ab4da6156 | |||
7922ae4801 | |||
94c35e210e | |||
34892e227a | |||
856eb720b0 | |||
94ad14f23f | |||
c8fa61cc4f | |||
8ab72e8f94 | |||
6b5b50d186 | |||
56f8d85feb | |||
51de11da25 | |||
ca2fd60950 | |||
e508d453ba | |||
12e1c60f6c | |||
8c3ba9f367 | |||
41acb02eb0 | |||
748609c6f4 | |||
ee7407a7cc | |||
201bb5986f | |||
51e6d95205 | |||
c78420749b | |||
adbde5a681 | |||
80aa9b968d | |||
b922f29bb8 | |||
4dd5ea71d6 | |||
297c18e176 | |||
a1e2bf9b3a | |||
c2784c400f | |||
b49865181b | |||
77c6f699a0 | |||
add1bd05cb | |||
4c69a1e579 | |||
5832bc68d5 | |||
8168cd3ab3 | |||
be497548c1 | |||
b3c28f6773 | |||
9b9334682f | |||
06ca5be54b | |||
30d5cdcb00 | |||
2427cceb5e | |||
73cc742dfb | |||
ade04a6ea1 | |||
3cf91a3f27 | |||
f9598977db | |||
7325c87068 | |||
1a70c6ea0b | |||
021dd32190 | |||
db80240209 | |||
1eedcaf2be | |||
86e6ffd18d | |||
15d2e1116b | |||
452533db17 | |||
b8979040da | |||
816492d3b2 | |||
0bddbb0bca | |||
314c01ef97 | |||
4c5a52417e | |||
23a5cd519a | |||
b57a241f52 | |||
2abfb2964c | |||
4858dbc060 | |||
|
2834ed2a7c | ||
|
165f85f8dc | ||
|
7f24b82281 | ||
7e1d232942 | |||
31a7ecee03 | |||
346bd9602d | |||
673351db51 | |||
86ebc4fae9 | |||
118540db8c | |||
a1d6c4fbe4 | |||
2bb07ae191 | |||
1920bd80a3 | |||
269000ab85 | |||
0ad4597daf | |||
e08d4c21b8 | |||
254045283b | |||
41b8b738d6 | |||
0411145ebf | |||
cca4444af2 | |||
3332c5c573 | |||
dca81de5a3 | |||
1c84891df6 | |||
fca0c2265c | |||
fda8872a15 | |||
ef63c3769e | |||
fddf2006e4 | |||
a0bf227573 | |||
32ec76611a | |||
13f4edbc92 | |||
8a1a8bc9a0 | |||
7d4d9eb438 | |||
e2415928a7 | |||
80112652bf | |||
92c98f26e5 | |||
8e2d590e62 | |||
e2e7011e53 | |||
|
31b110a06c | ||
|
717ad5d88c | ||
|
cc839620cf | ||
|
cebd79079b | ||
|
7a7e00bea2 | ||
|
7ff08a9587 | ||
|
370a7d4c15 | ||
|
85f56cf98c | ||
|
6ae76f1f38 | ||
|
67d3b40772 | ||
|
d2887b7454 | ||
d10649e1f1 | |||
|
e127f2597c | ||
|
1777ebb051 | ||
|
3eb7f01d38 | ||
|
57304e8d7e | ||
|
b4fc734a15 | ||
|
bc3e0b8634 | ||
|
1726cbd96a | ||
|
e5c8adfc1b | ||
|
049a48e156 | ||
|
5d4f4b0378 | ||
|
0db997c6a0 | ||
|
5e7c6c26c9 | ||
|
2de1e3ebe6 | ||
|
79179746a7 | ||
|
fc718c907a | ||
|
0cd120f492 | ||
|
c3f01c198f | ||
|
229b2de566 | ||
|
dea547a491 | ||
|
1036358b28 | ||
|
8f477fa335 | ||
|
3c6f3c5a21 | ||
|
0902099855 | ||
|
3c9a633907 | ||
|
1e193f8346 | ||
|
9af02ba886 | ||
|
221bcc058b | ||
|
04cb2324aa | ||
|
887a0e5e88 | ||
|
cc3e52c69d | ||
|
243e56b194 | ||
|
ecf6a844dd | ||
|
62b651789a | ||
|
ba8e5ef6a0 | ||
|
9dacd2d3c9 | ||
|
39abdfe40a | ||
|
0c0e7411f7 | ||
|
9179f35a82 | ||
|
470bd62af7 | ||
|
c7711d75a1 | ||
|
711ea543bb | ||
|
43f71ed47b | ||
|
a07cb6ec3d | ||
|
45d998130b | ||
|
cc36325ca6 | ||
|
c5dc9333f0 | ||
|
8a7f783032 | ||
|
77530b4a06 | ||
|
6d7f1502ce | ||
|
8279f22940 | ||
|
1c84659e34 | ||
|
37e006d86e | ||
|
8d7e790eda | ||
|
daaa0050d1 | ||
|
287b6303ae | ||
|
9b23ab7015 | ||
|
7947c1031d | ||
|
9e6c40d393 | ||
|
f3a9dc4ef5 | ||
|
05b0a7f14d | ||
|
6ad5397a25 | ||
|
0a9c4fc3a7 | ||
|
b30145dfc9 | ||
|
e9d9ef252f | ||
|
f2a654bdec | ||
|
d92e630314 | ||
|
07336d3272 | ||
|
00012aacae | ||
|
959cc8b75b | ||
|
df1e03f0af | ||
|
b21193dc74 | ||
|
9ad6188b5d | ||
|
31f2c5152c | ||
|
c269c62fe6 | ||
|
6e6092e4b2 | ||
|
ac42f7eba4 | ||
|
258a9b57ce | ||
|
28409b8688 | ||
|
c12e7bab90 | ||
|
09f9e974b1 | ||
|
c0f7cc0782 | ||
|
0af326fa11 | ||
|
beeb7dca7c | ||
|
1e972ded99 | ||
|
a650840429 | ||
|
4d0d467292 | ||
|
83122becdb | ||
|
9623752e19 | ||
|
53ddbad7ce | ||
|
ac02becba8 | ||
|
13642cc42e | ||
|
d6f653d834 | ||
|
dc254d6474 | ||
|
8a1f49a906 | ||
|
6dce4c59f6 | ||
|
ce42f5aada | ||
|
e9d4ed3911 | ||
|
2ceb3511b3 | ||
|
d27b23ec5a | ||
|
9402e4f65f | ||
|
a4c6028dc4 | ||
|
aeed86e377 | ||
|
907bc0f75b | ||
|
d7806757c5 | ||
|
ca168828de | ||
|
6c24783df7 | ||
|
a65a6e9d02 | ||
|
681e9f1703 | ||
|
737c135996 | ||
|
edd2235cbc | ||
|
dc1b54961f | ||
|
3c3ee09d90 | ||
|
e533484505 | ||
|
1681f80728 | ||
|
f1ab923bfe | ||
|
d7c3ad64f5 | ||
|
ca0c4e3089 | ||
|
beceb39b0c | ||
|
9f1128e2c0 | ||
|
e77e5bb3d8 | ||
|
0d182820e8 | ||
|
f7df1d2f6e | ||
|
cd8732dc8f | ||
|
b8917de52f | ||
|
aa631deb88 | ||
|
2e6388f6f4 | ||
|
5d2e74eb97 | ||
|
40269ddaad | ||
|
af614480b8 | ||
|
7a7c065754 | ||
|
6bed284e8b | ||
|
289eebd6a0 | ||
|
f54a5cdf87 | ||
|
43ef4bccdf | ||
|
1bb22d818a | ||
|
fa0a5805be | ||
|
bf92d66be1 | ||
|
a9509fc959 | ||
|
2dc8fe9437 | ||
|
a0d97c0f18 | ||
|
d8f7b15111 | ||
|
ddedcd647c | ||
|
63ad4bfdce | ||
|
1abcb1de4b | ||
|
8eb457017a | ||
|
019bcbf90a | ||
|
403076e6ab | ||
|
988601b10e | ||
|
cbae8132ed | ||
|
5302d3143e | ||
|
4b86b8ef54 | ||
|
62e5d03931 | ||
|
d5292e068d | ||
|
40c2c157ec | ||
|
ead5b94942 | ||
|
9b2c6ef62d | ||
|
ccc01e1823 | ||
|
9c52d96c08 | ||
|
06258b8304 | ||
|
0659996c48 | ||
|
af2dbf899d | ||
|
20b095232d | ||
|
72bb159263 | ||
|
f35bacaae5 | ||
|
c057cd13fd | ||
|
b79fd4e25d | ||
|
7e8bef40cd | ||
|
bc3c01254e | ||
|
dc5afebd27 | ||
|
5bb4053ed8 | ||
|
6b43a5e592 | ||
|
d3e52ba944 | ||
|
6883db7f8b | ||
|
65bac0ccc5 | ||
|
cabc154252 | ||
|
20e67ffc93 | ||
|
1973d1a6b5 |
2135 changed files with 150072 additions and 22014 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,7 +3,7 @@
|
|||
*.pyc
|
||||
__pycache__
|
||||
build/
|
||||
dist/
|
||||
/dist/
|
||||
*.egg-info/
|
||||
package.sh
|
||||
.pypirc
|
||||
|
@ -17,3 +17,5 @@ platypush/backend/http/static/js/lib/vue.js
|
|||
platypush/notebooks
|
||||
platypush/requests
|
||||
/http-client.env.json
|
||||
/platypush/backend/http/static/css/dist
|
||||
/tests/etc/dashboards
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,9 +1,3 @@
|
|||
[submodule "docs/wiki"]
|
||||
path = docs/wiki
|
||||
url = https://github.com/BlackLight/platypush.wiki.git
|
||||
[submodule "platypush/plugins/gpio/sensor/ir/mlx90640/lib"]
|
||||
path = platypush/plugins/camera/ir/mlx90640/lib
|
||||
url = https://github.com/pimoroni/mlx90640-library
|
||||
[submodule "webext"]
|
||||
path = webext
|
||||
url = https://github.com/BlackLight/platypush-webext.git
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
setup_py_install: true
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
pyyaml
|
||||
requests
|
||||
flask
|
||||
redis
|
||||
python-dateutil
|
||||
websockets
|
||||
bcrypt
|
||||
sqlalchemy
|
||||
croniter
|
22
.travis.yml
22
.travis.yml
|
@ -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
177
CHANGELOG.md
Normal 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
37
CONTRIBUTING.md
Normal 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.
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017, 2018 Fabio Manganiello
|
||||
Copyright (c) 2017, 2020 Fabio Manganiello
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
recursive-include platypush/backend/http/static *
|
||||
recursive-include platypush/backend/http/templates *
|
||||
recursive-include platypush/backend/http/webapp/dist *
|
||||
include platypush/plugins/http/webpage/mercury-parser.js
|
||||
|
|
539
README.md
539
README.md
|
@ -1,68 +1,509 @@
|
|||
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://platypush.readthedocs.io/en/latest/?badge=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/-/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)
|
||||
|
||||
Advised read: [**Getting started with Platypush**](https://medium.com/@automationguru/automate-your-house-your-life-and-everything-else-around-with-platypush-dba1cd13e3f6) (Medium article).
|
||||
[Reddit channel](https://www.reddit.com/r/platypush)
|
||||
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
|
||||
|
||||
Imagine Platypush as some kind of [IFTTT](https://ifttt.com) on steroids - or [Tasker](https://tasker.joaoapps.com/), or [Microsoft Flow](https://flow.microsoft.com), or [PushBullet](https://pushbullet.com) on steroids.
|
||||
Platypush aims to turn any device in a smart hub that can control things, interact with cloud services and send messages to other devices. It's a general-purpose lightweight platform to process any request and run any logic triggered by custom events.
|
||||
- The [blog](https://blog.platypush.tech) is in general a good place to get more insights on what you can build with it and inspiration about possible usages.
|
||||
|
||||
Imagine the ability of running any task you like, or automate any routine you like, on any of your devices. And the flexibility of executing actions through a cloud service, with the power of running them from your laptop, Raspberry Pi, smart home device or smartphone.
|
||||
- The [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
|
||||
|
||||
- Extensive documentation for all the available integrations and messages [is available](https://docs.platypush.tech/).
|
||||
|
||||
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
|
||||
|
||||
- A [Reddit channel](https://www.reddit.com/r/platypush) is also available for more general questions.
|
||||
|
||||
---
|
||||
|
||||
Platypush is a general-purpose extensible platform for automation and integration across multiple services and devices.
|
||||
|
||||
It enables users to create their own self-hosted pieces of automation based on events (*if this happens then do that*)
|
||||
and it provides a comprehensive and customizable user interface that collects everything you need to visualize and
|
||||
control under one roof.
|
||||
|
||||
It takes some concepts from [IFTTT](https://ifttt.com), [Tasker](https://tasker.joaoapps.com/),
|
||||
[Microsoft Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
|
||||
[Home Assistant](https://www.home-assistant.io/) to provide an environment where the user can easily connect things
|
||||
together.
|
||||
|
||||
Its ideal home is a single-board computer like a RaspberryPi that you can configure to orchestrate any home automation
|
||||
and cloud automation in your own living room or garage, but it can easily run on any device that can run a Python
|
||||
interpreter, and the bar for the hardware requirements is very low as well - I use it to run pieces of automation on
|
||||
devices as powerful as a RaspberryPi Zero or an old Nokia N900 with Linux.
|
||||
|
||||
You can use Platypush to do things like:
|
||||
|
||||
- Control your smart home lights
|
||||
- Control your favourite music player
|
||||
- Interact with your voice assistant
|
||||
- Get events from your Google or Facebook calendars
|
||||
- Read data from your sensors and trigger custom events whenever they go above or below some custom thresholds
|
||||
- Control the motors of your robot
|
||||
- Send automated emails
|
||||
- Synchronize the clipboards on your devices
|
||||
- Control your smart switches
|
||||
- Implement custom text-to-speech commands
|
||||
- Build any kind of interaction with your Android device using Tasker
|
||||
- Play local videos, YouTube videos and torrent links
|
||||
- Get weather forecast for your location
|
||||
- [Control your smart home lights](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush)
|
||||
- [Control your music and synchronize it to multiple devices](https://blog.platypush.tech/article/Build-your-open-source-multi-room-and-multi-provider-sound-server-with-Platypush-Mopidy-and-Snapcast)
|
||||
- [Create custom and privacy-secure voice assistants that run custom hooks on your phrases](https://blog.platypush.tech/article/Build-custom-voice-assistants)
|
||||
- Build integrations between [sensors](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
|
||||
[cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
||||
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html) and
|
||||
[machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html) to create smart
|
||||
pieces of automation for e.g.
|
||||
[people detection](https://blog.platypush.tech/article/Detect-people-with-a-RaspberryPi-a-thermal-camera-Platypush-and-a-pinch-of-machine-learning)
|
||||
or [sound detection](https://blog.platypush.tech/article/Create-your-smart-baby-monitor-with-Platypush-and-Tensorflow)
|
||||
- [Get events from your Google or Facebook calendars](https://docs.platypush.tech/en/latest/platypush/plugins/calendar.html)
|
||||
- [Read data from your sensors and trigger custom events whenever they go above or below some custom thresholds](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
|
||||
- [Control and automate a self-built robot](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.zeroborg.html)
|
||||
- [Deliver automated newsletters from custom RSS digests](https://blog.platypush.tech/article/Deliver-customized-newsletters-from-RSS-feeds-with-Platypush)
|
||||
- [Synchronize the clipboards on your devices](https://docs.platypush.tech/en/latest/platypush/plugins/clipboard.html)
|
||||
- [Control your smart switches](https://docs.platypush.tech/en/latest/platypush/plugins/switch.html)
|
||||
- [Implement automated custom text-to-speech routines](https://docs.platypush.tech/en/latest/platypush/plugins/tts.html)
|
||||
- [Build any kind of interactions and automation routines with your Android device using Tasker](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
|
||||
- Play [local videos](https://docs.platypush.tech/en/latest/platypush/plugins/media.mpv.html), YouTube videos and torrent media from any device and service, to any device
|
||||
- [Get weather forecast events for your location and build automation routines on them](https://docs.platypush.tech/en/latest/platypush/plugins/weather.darksky.html)
|
||||
- [Create a custom single hub for Zigbee and Z-Wave smart devices](https://blog.platypush.tech/article/Transform-a-RaspberryPi-into-a-universal-Zigbee-and-Z-Wave-bridge)
|
||||
- Build your own web dashboard with calendar, weather, news and music controls (basically, anything that has a Platypush web widget)
|
||||
- ...and much more (basically, anything that comes with a [Platypush plugin](https://platypush.readthedocs.io/en/latest/plugins.html))
|
||||
- ...and much more (basically, anything that comes with a [Platypush plugin](https://docs.platypush.tech/en/latest/plugins.html))
|
||||
|
||||
Imagine the ability of executing all the actions above through messages delivered through:
|
||||
## Architecture
|
||||
|
||||
- A web interface
|
||||
- A JSON-RPC API
|
||||
- Raw TCP messages
|
||||
- Web sockets
|
||||
- [PushBullet](https://pushbullet.com)
|
||||
- [Kafka](https://kafka.apache.org)
|
||||
- [Redis](https://redis.io)
|
||||
- [MQTT](https://mqtt.org)
|
||||
- ...amd much more (basically, anything that comes with a [Platypush backend](https://platypush.readthedocs.io/en/latest/backends.html))
|
||||
The architecture of Platypush consists of a few simple pieces, orchestrated by a configuration file stored by default
|
||||
under [`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml):
|
||||
|
||||
Imagine the ability of building custom event hooks to automatically trigger any actions:
|
||||
### [Plugins](https://docs.platypush.tech/en/latest/plugins.html)
|
||||
|
||||
- When your voice assistant recognizes some text
|
||||
- When you start playing a new song
|
||||
- When a new event is added to your calendar
|
||||
- When a new article is published on your favourite feed
|
||||
- When the weather conditions change
|
||||
- When your press a [Flic button](https://flic.io) with a certain pattern
|
||||
- When you receive a new push on your Pushbullet account
|
||||
- When your GPS signal enters a certain area
|
||||
- Whenever a new MIDI event is received (yes, you heard well :) )
|
||||
- Whenever a sensor sends new data
|
||||
- At a specific date or time
|
||||
- ...and so on (basically, anything can send events that can be used to build hooks)
|
||||
They are integrations that do things - like
|
||||
[modify files](https://docs.platypush.tech/en/latest/platypush/plugins/file.html),
|
||||
[train and evaluate machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html),
|
||||
[control cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
||||
[read sensors](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.sensor.dht.html),
|
||||
[parse a web page](https://docs.platypush.tech/en/latest/platypush/plugins/http.webpage.html),
|
||||
[control lights](https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html),
|
||||
[send emails](https://docs.platypush.tech/en/latest/platypush/plugins/mail.smtp.html),
|
||||
[control Chromecasts](https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html),
|
||||
[run voice queries](https://docs.platypush.tech/en/latest/platypush/plugins/assistant.google.html),
|
||||
[handle torrent transfers](https://docs.platypush.tech/en/latest/platypush/plugins/torrent.html) or
|
||||
control [Zigbee](https://docs.platypush.tech/en/latest/platypush/plugins/zigbee.mqtt.html) or
|
||||
[Z-Wave](https://docs.platypush.tech/en/latest/platypush/plugins/zwave.html) devices.
|
||||
|
||||
Imagine the ability of running the application, with lots of those bundled features, on any device that can comes with Python (_only compatible with version 3.5 and higher_). Platypush has been designed with performance in mind, it's been heavily tested on slower devices like Raspberry Pis, and it can run the web server features, multiple backends and plugins quite well even on a Raspberry Pi Zero - it's even been tested with some quite impressive performance on an older [Nokia N900](https://en.wikipedia.org/wiki/Nokia_N900), and of course you can run it on any laptop, desktop, server environment. It's been developed mainly with IoT in mind (and some of its features overlap with IoT frameworks like [Mozilla IoT](https://iot.mozilla.com) and [Android Things](https://developer.android.com/things/)), but nothing prevents you from automating any task on any device and environment.
|
||||
The configuration of a plugin matches one-on-one that of its documented class constructor, so it's very straightforward
|
||||
to write a configuration for a plugin by reading its documentation:
|
||||
|
||||
To get started:
|
||||
```yaml
|
||||
light.hue:
|
||||
# Groups that will be controlled by default
|
||||
groups:
|
||||
- Living Room
|
||||
- Hall
|
||||
```
|
||||
|
||||
- [Wiki](https://github.com/BlackLight/platypush/wiki) for installation notes, quick start, examples and architecture reference
|
||||
- [Read the docs](https://platypush.readthedocs.io/en/latest/) for a complete reference on the available plugins and backends
|
||||
- [Medium articles](https://medium.com/tag/platypush/archive) that describe hands-on applications of platypush
|
||||
### Actions
|
||||
|
||||
Plugins expose *actions*, that match one-on-one the plugin class methods denoted by `@action`, so it's very
|
||||
straightforward to invoke plugin actions by just reading the plugin documentation. They can be invoked directly from
|
||||
your own scripts or they can be sent to the platform through any supported channel as simple JSON messages:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "request",
|
||||
"action": "light.hue.on",
|
||||
"args": {
|
||||
"lights": ["Entrance Bulb"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### [Backends](https://docs.platypush.tech/en/latest/backends.html)
|
||||
|
||||
They are background services that either listen for messages on channels (like an
|
||||
[HTTP backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
|
||||
[MQTT instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
|
||||
[Kafka instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html), a
|
||||
[Websocket service](https://docs.platypush.tech/en/latest/platypush/backend/websocket.html),
|
||||
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html) etc.) or monitor a device or a
|
||||
service for events (like a [sensor](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html), a custom
|
||||
[voice assistant](https://docs.platypush.tech/en/latest/platypush/backend/assistant.google.html), a bridge running on a
|
||||
[Zigbee](https://docs.platypush.tech/en/latest/platypush/backend/zigbee.mqtt.html) or
|
||||
[Z-Wave](https://docs.platypush.tech/en/latest/platypush/backend/zwave.html), an
|
||||
[NFC card reader](https://docs.platypush.tech/en/latest/platypush/backend/nfc.html), a
|
||||
[MIDI device](https://docs.platypush.tech/en/latest/platypush/backend/midi.html), a
|
||||
[Telegram channel](https://docs.platypush.tech/en/latest/platypush/backend/chat.telegram.html), a
|
||||
[Bluetooth scanner](https://docs.platypush.tech/en/latest/platypush/backend/bluetooth.scanner.ble.html) etc.).
|
||||
|
||||
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka, Websocket and TCP) then you can send requests
|
||||
to these services in JSON format. For example, in the case of the HTTP backend:
|
||||
|
||||
```shell
|
||||
# Get a token
|
||||
curl -XPOST -H 'Content-Type: application/json' -d '
|
||||
{
|
||||
"username": "$YOUR_USER",
|
||||
"password": "$YOUR_PASSWORD"
|
||||
}' http://host:8008/auth
|
||||
|
||||
# Execute a request
|
||||
|
||||
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||
{
|
||||
"type": "request",
|
||||
"action": "tts.say",
|
||||
"args": {
|
||||
"text": "This is a test"
|
||||
}
|
||||
}' http://host:8008/execute
|
||||
```
|
||||
|
||||
### [Events](https://docs.platypush.tech/en/latest/events.html)
|
||||
|
||||
When a certain event occurs (e.g. a JSON request is received, or a
|
||||
[Bluetooth device is connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
|
||||
or a
|
||||
[Flic button is pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
|
||||
or some
|
||||
[speech is detected on the voice assistant service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
|
||||
or an
|
||||
[RSS feed has new items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
|
||||
or a
|
||||
[new email is received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
|
||||
or a
|
||||
[new track is played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
|
||||
or an
|
||||
[NFC tag is detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
|
||||
or
|
||||
[new sensor data is available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
|
||||
or
|
||||
[a value of a Zigbee device changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
|
||||
etc.), the associated backend will trigger an [event](https://docs.platypush.tech/en/latest/events.html).
|
||||
|
||||
### Hooks
|
||||
|
||||
Event hooks are custom pieces of logic that will be run when a certain event is triggered. Hooks are the glue that
|
||||
connects events to actions, exposing a paradigm similar to IFTTT (_if a certain event happens then run these actions_).
|
||||
They can declared as:
|
||||
|
||||
- Sections of the [`config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml).
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
event.hook.SearchSongVoiceCommand:
|
||||
if:
|
||||
type: platypush.message.event.assistant.SpeechRecognizedEvent
|
||||
phrase: "play ${title} by ${artist}"
|
||||
then:
|
||||
- action: music.mpd.clear
|
||||
- action: music.mpd.search
|
||||
args:
|
||||
filter:
|
||||
artist: ${artist}
|
||||
title: ${title}
|
||||
|
||||
- if ${len(output)}:
|
||||
- action: music.mpd.play
|
||||
args:
|
||||
resource: ${output[0]['file']}
|
||||
```
|
||||
|
||||
- Stand-alone Python scripts stored under `~/.config/platypush/scripts` and will be dynamically imported at start time.
|
||||
[Example](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/hook.py):
|
||||
|
||||
```python
|
||||
from platypush.event.hook import hook
|
||||
from platypush.utils import run
|
||||
from platypush.message.event.assistant import SpeechRecognizedEvent
|
||||
|
||||
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
|
||||
def on_music_play_command(event, title=None, artist=None, **context):
|
||||
results = run('music.mpd.search', filter={
|
||||
'artist': artist,
|
||||
'title': title,
|
||||
})
|
||||
|
||||
if results:
|
||||
run('music.mpd.play', results[0]['file'])
|
||||
```
|
||||
|
||||
### Procedures
|
||||
|
||||
Procedures are pieces of custom logic that can be executed as atomic actions using `procedure.<name>` as an action name.
|
||||
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` -
|
||||
provided that the procedure is also imported in `~/.config/platypush/scripts/__init__.py` so it can be discovered by
|
||||
the service.
|
||||
|
||||
YAML example for a procedure that can be executed when we arrive home and turns on the lights if the luminosity is lower
|
||||
that a certain thresholds, says a welcome home message using the TTS engine and starts playing the music:
|
||||
|
||||
```yaml
|
||||
procedure.at_home:
|
||||
# Get luminosity data from a sensor - e.g. LTR559
|
||||
- action: gpio.sensor.ltr559.get_data
|
||||
|
||||
# If it's lower than a certain threshold, turn on the lights
|
||||
- if ${int(light or 0) < 110}:
|
||||
- action: light.hue.on
|
||||
|
||||
# Say a welcome home message
|
||||
- action: tts.google.say
|
||||
args:
|
||||
text: Welcome home
|
||||
|
||||
# Play the music
|
||||
- action: music.mpd.play
|
||||
```
|
||||
|
||||
Python example:
|
||||
|
||||
```python
|
||||
# Content of ~/.config/platypush/scripts/home.py
|
||||
from platypush.procedure import procedure
|
||||
from platypush.utils import run
|
||||
|
||||
@procedure
|
||||
def at_home(**context):
|
||||
sensor_data = run('gpio.sensor.ltr559.get_data')
|
||||
if sensor_data['light'] < 110:
|
||||
run('light.hue.on')
|
||||
|
||||
run('tts.google.say', text='Welcome home')
|
||||
run('music.mpd.play')
|
||||
```
|
||||
|
||||
In either case, you can easily trigger the at-home procedure by sending an action request message to a backend - for
|
||||
example, over the HTTP backend:
|
||||
|
||||
```shell
|
||||
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||
{
|
||||
"type": "request",
|
||||
"action": "procedure.at_home"
|
||||
}' 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
|
||||
will be provided by default on `http://host:8008/`. Besides using the `/execute` endpoint for running requests, the
|
||||
built-in web server also provides a full-featured interface that groups together the controls for most of the plugins -
|
||||
e.g. sensors, switches, music controls and search, media library and torrent management, lights, Zigbee/Z-Wave devices
|
||||
and so on. The UI is responsive and mobile-friendly.
|
||||
|
||||
The web service also provides means for the user to create
|
||||
[custom dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml) that can
|
||||
be used to show information from multiple sources on a large screen.
|
||||
|
||||
## Installation
|
||||
|
||||
### System installation
|
||||
|
||||
Platypush uses Redis to deliver and store requests and temporary messages:
|
||||
|
||||
```yaml
|
||||
# Example for Debian-based distributions
|
||||
[sudo] apt-get install redis-server
|
||||
|
||||
# Enable and start the service
|
||||
[sudo] systemctl enable redis
|
||||
[sudo] systemctl start redis
|
||||
```
|
||||
|
||||
To install the core platform:
|
||||
|
||||
* The `pip` way:
|
||||
|
||||
```shell
|
||||
[sudo] pip3 install platypush
|
||||
```
|
||||
|
||||
* The sources way:
|
||||
|
||||
```shell
|
||||
git clone https://git.platypush.tech/platypush/platypush.git
|
||||
cd platypush
|
||||
[sudo] python3 setup.py install
|
||||
```
|
||||
|
||||
Then install the extensions that you wish to use. There are a few ways to check the dependencies required by an
|
||||
extension:
|
||||
|
||||
#### Check their `extras` name in [`extras_require` under `setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72).
|
||||
|
||||
If you follow this route then you can install the extra dependencies in one of the following ways:
|
||||
|
||||
1. `pip` installation:
|
||||
|
||||
```shell
|
||||
[sudo] pip3 install 'platypush[extra1,extra2,extra3]'
|
||||
```
|
||||
|
||||
2. Sources installation:
|
||||
|
||||
```shell
|
||||
cd $DIR_TO_PLATYPUSH
|
||||
[sudo] pip3 install '.[extra1,extra2,extra3]'
|
||||
```
|
||||
|
||||
#### Check the dependencies/installation instructions reported under the plugin/backend documentation.
|
||||
|
||||
If you follow this route then simply run the commands listed in the plugin/backend documentation to get the dependencies
|
||||
installed.
|
||||
|
||||
#### Check/uncomment the associated lines in [`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt).
|
||||
|
||||
If you follow this route then uncomment the lines in
|
||||
[`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt) associated to the
|
||||
plugins/backends that you want to use and run:
|
||||
|
||||
```shell
|
||||
[sudo] pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
After installing the dependencies, create a configuration file under `~/.config/platypush/config.yaml` (the application
|
||||
can load the configuration from another location through the `-c` option) containing the configuration of the backend
|
||||
and plugins that you want to use, and add any hooks and procedures for your use case.
|
||||
|
||||
You can then start the service by simply running:
|
||||
|
||||
```shell
|
||||
platypush
|
||||
```
|
||||
|
||||
It's advised to run it as a systemd service though - simply copy the provided
|
||||
[`.service` file](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/systemd/platypush.service) to
|
||||
`~/.config/systemd/user`, check if the path of `platypush` matches the path where it's installed on your system, and
|
||||
start the service via `systemctl`:
|
||||
|
||||
```shell
|
||||
systemctl --user start platypush
|
||||
```
|
||||
|
||||
### [Virtual environment installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-virtual-environment)
|
||||
|
||||
Platypush provides a script named `platyvenv` that can parse a `config.yaml` and automatically create a virtual
|
||||
environment (under `~/.local/share/platypush/venv/<device_id>`) with all the dependencies required by the configured
|
||||
integrations.
|
||||
|
||||
1. Create the environment from a configuration file:
|
||||
|
||||
```shell
|
||||
platyvenv build -c /path/to/config.yaml
|
||||
```
|
||||
|
||||
2. Start the service from the virtual environment:
|
||||
|
||||
```shell
|
||||
# device_id matches either the hostname or the device_id in config.yaml
|
||||
platyvenv start device_id
|
||||
```
|
||||
|
||||
3. Stop the instance:
|
||||
|
||||
```shell
|
||||
platyvenv stop device_id
|
||||
```
|
||||
|
||||
4. Remove the instance:
|
||||
|
||||
```shell
|
||||
platyvenv rm device_id
|
||||
```
|
||||
|
||||
### [Docker installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-container)
|
||||
|
||||
You can also install Platypush in a container - the application provides a script named `platydock` that automatically
|
||||
creates a container instance from a `config.yaml`:
|
||||
|
||||
1. Create the container from a configuration file:
|
||||
|
||||
```shell
|
||||
platydock build -c /path/to/config.yaml
|
||||
```
|
||||
|
||||
2. Start the container:
|
||||
|
||||
```shell
|
||||
# device_id matches either the hostname or the device_id in config.yaml
|
||||
platydock start device_id
|
||||
```
|
||||
|
||||
3. Stop the instance:
|
||||
|
||||
```shell
|
||||
platydock stop device_id
|
||||
```
|
||||
|
||||
4. Remove the instance:
|
||||
|
||||
```shell
|
||||
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 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Funding
|
||||
|
||||
If you use and love Platypush, please consider [buying me a coffee/beer](https://paypal.me/fabiomanganiello).
|
||||
|
||||
I've been working on Platypush all by myself in my spare time for the past few years, and I've made sure that it remains
|
||||
open and free.
|
||||
|
||||
If you like this product, please consider supporting - I'm definitely not planning to get rich with this project, but
|
||||
I'd love to have at least the costs for the server covered by users.
|
||||
|
||||
Issues and requests opened by donors will also be given priority over others.
|
||||
|
|
|
@ -119,7 +119,7 @@ EOF
|
|||
pip install ${dep}
|
||||
done
|
||||
|
||||
pip install --upgrade git+https://github.com/BlackLight/platypush.git
|
||||
pip install --upgrade git+https://git.platypush.tech/platypush/platypush.git
|
||||
echo "Platypush virtual environment prepared under $envdir"
|
||||
}
|
||||
|
||||
|
|
21
docs/README.md
Normal file
21
docs/README.md
Normal 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.
|
||||
|
59
docs/source/_ext/sphinx_marshmallow.py
Normal file
59
docs/source/_ext/sphinx_marshmallow.py
Normal 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,
|
||||
}
|
|
@ -21,7 +21,10 @@ Backends
|
|||
platypush/backend/chat.telegram.rst
|
||||
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
|
||||
platypush/backend/google.pubsub.rst
|
||||
platypush/backend/gps.rst
|
||||
|
@ -29,15 +32,20 @@ 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
|
||||
platypush/backend/ping.rst
|
||||
|
@ -49,6 +57,7 @@ Backends
|
|||
platypush/backend/sensor.arduino.rst
|
||||
platypush/backend/sensor.battery.rst
|
||||
platypush/backend/sensor.bme280.rst
|
||||
platypush/backend/sensor.dht.rst
|
||||
platypush/backend/sensor.distance.rst
|
||||
platypush/backend/sensor.distance.vl53l1x.rst
|
||||
platypush/backend/sensor.envirophat.rst
|
||||
|
@ -66,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
|
||||
|
|
|
@ -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-2019, 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': '🕮',
|
||||
'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 ---------------------------------------------
|
||||
|
||||
|
@ -252,6 +301,20 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'pandas',
|
||||
'samsungtvws',
|
||||
'paramiko',
|
||||
'luma',
|
||||
'zeroconf',
|
||||
'dbus',
|
||||
'gi',
|
||||
'gi.repository',
|
||||
'twilio',
|
||||
'pytz',
|
||||
'Adafruit_Python_DHT',
|
||||
'RPi.GPIO',
|
||||
'RPLCD',
|
||||
'imapclient',
|
||||
'pysmartthings',
|
||||
'aiohttp',
|
||||
'watchdog',
|
||||
]
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
@ -264,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:
|
||||
|
|
|
@ -6,7 +6,6 @@ Events
|
|||
:maxdepth: 2
|
||||
:caption: Events:
|
||||
|
||||
platypush/events/.rst
|
||||
platypush/events/adafruit.rst
|
||||
platypush/events/alarm.rst
|
||||
platypush/events/application.rst
|
||||
|
@ -17,28 +16,33 @@ Events
|
|||
platypush/events/chat.telegram.rst
|
||||
platypush/events/clipboard.rst
|
||||
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
|
||||
platypush/events/google.rst
|
||||
platypush/events/google.fit.rst
|
||||
platypush/events/google.pubsub.rst
|
||||
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
|
||||
platypush/events/mqtt.rst
|
||||
platypush/events/music.rst
|
||||
platypush/events/music.snapcast.rst
|
||||
platypush/events/nextcloud.rst
|
||||
platypush/events/nfc.rst
|
||||
platypush/events/path.rst
|
||||
platypush/events/ping.rst
|
||||
platypush/events/pushbullet.rst
|
||||
platypush/events/qrcode.rst
|
||||
|
@ -61,5 +65,6 @@ Events
|
|||
platypush/events/web.widget.rst
|
||||
platypush/events/wiimote.rst
|
||||
platypush/events/zeroborg.rst
|
||||
platypush/events/zeroconf.rst
|
||||
platypush/events/zigbee.mqtt.rst
|
||||
platypush/events/zwave.rst
|
||||
|
|
|
@ -5,13 +5,13 @@ Welcome to the Platypush reference of available plugins, backends and event type
|
|||
|
||||
For more information on Platypush please check out:
|
||||
|
||||
* The `GitHub page`_ of the project
|
||||
* The `Gitlab page`_ of the project
|
||||
* The `online wiki`_ for quickstart and examples
|
||||
* The `Medium stories`_ for inspiration about possible projects
|
||||
* The `Blog articles`_ for inspiration on use-cases possible projects
|
||||
|
||||
.. _GitHub page: https://github.com/BlackLight/platypush
|
||||
.. _online wiki: https://github.com/BlackLight/platypush/wiki
|
||||
.. _Medium stories: https://medium.com/tag/platypush/archive
|
||||
.. _Gitlab page: https://git.platypush.tech/platypush/platypush
|
||||
.. _online wiki: https://git.platypush.tech/platypush/platypush/-/wikis/home
|
||||
.. _Blog articles: https://blog.platypush.tech
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
|
5
docs/source/platypush/backend/dbus.rst
Normal file
5
docs/source/platypush/backend/dbus.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.dbus``
|
||||
==========================
|
||||
|
||||
.. automodule:: platypush.backend.dbus
|
||||
:members:
|
5
docs/source/platypush/backend/file.monitor.rst
Normal file
5
docs/source/platypush/backend/file.monitor.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.file.monitor``
|
||||
==================================
|
||||
|
||||
.. automodule:: platypush.backend.file.monitor
|
||||
:members:
|
5
docs/source/platypush/backend/github.rst
Normal file
5
docs/source/platypush/backend/github.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.github``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.backend.github
|
||||
:members:
|
5
docs/source/platypush/backend/joystick.jstest.rst
Normal file
5
docs/source/platypush/backend/joystick.jstest.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.joystick.jstest``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.jstest
|
||||
:members:
|
5
docs/source/platypush/backend/joystick.linux.rst
Normal file
5
docs/source/platypush/backend/joystick.linux.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.joystick.linux``
|
||||
====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.linux
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.backend.local``
|
||||
===========================
|
||||
|
||||
.. automodule:: platypush.backend.local
|
||||
:members:
|
5
docs/source/platypush/backend/log.http.rst
Normal file
5
docs/source/platypush/backend/log.http.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.log.http``
|
||||
==============================
|
||||
|
||||
.. automodule:: platypush.backend.log.http
|
||||
:members:
|
5
docs/source/platypush/backend/mail.rst
Normal file
5
docs/source/platypush/backend/mail.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.mail``
|
||||
==========================
|
||||
|
||||
.. automodule:: platypush.backend.mail
|
||||
:members:
|
5
docs/source/platypush/backend/music.spotify.connect.rst
Normal file
5
docs/source/platypush/backend/music.spotify.connect.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.music.spotify.connect``
|
||||
===========================================
|
||||
|
||||
.. automodule:: platypush.backend.music.spotify.connect
|
||||
:members:
|
5
docs/source/platypush/backend/nextcloud.rst
Normal file
5
docs/source/platypush/backend/nextcloud.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.nextcloud``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.backend.nextcloud
|
||||
:members:
|
5
docs/source/platypush/backend/sensor.dht.rst
Normal file
5
docs/source/platypush/backend/sensor.dht.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.sensor.dht``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.sensor.dht
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.backend.stt.picovoice``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.backend.stt.picovoice
|
||||
:members:
|
5
docs/source/platypush/backend/weather.openweathermap.rst
Normal file
5
docs/source/platypush/backend/weather.openweathermap.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.weather.openweathermap``
|
||||
============================================
|
||||
|
||||
.. automodule:: platypush.backend.weather.openweathermap
|
||||
:members:
|
5
docs/source/platypush/backend/weather.rst
Normal file
5
docs/source/platypush/backend/weather.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.weather``
|
||||
=============================
|
||||
|
||||
.. automodule:: platypush.backend.weather
|
||||
:members:
|
5
docs/source/platypush/backend/zwave.mqtt.rst
Normal file
5
docs/source/platypush/backend/zwave.mqtt.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.zwave.mqtt``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.backend.zwave.mqtt
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.message.event``
|
||||
===========================
|
||||
|
||||
.. automodule:: platypush.message.event
|
||||
:members:
|
5
docs/source/platypush/events/custom.rst
Normal file
5
docs/source/platypush/events/custom.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.custom``
|
||||
==================================
|
||||
|
||||
.. automodule:: platypush.message.event.custom
|
||||
:members:
|
5
docs/source/platypush/events/file.rst
Normal file
5
docs/source/platypush/events/file.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.file``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.message.event.file
|
||||
:members:
|
5
docs/source/platypush/events/github.rst
Normal file
5
docs/source/platypush/events/github.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.github``
|
||||
==================================
|
||||
|
||||
.. automodule:: platypush.message.event.github
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.message.event.http.ota.booking``
|
||||
============================================
|
||||
|
||||
.. automodule:: platypush.message.event.http.ota.booking
|
||||
:members:
|
5
docs/source/platypush/events/inotify.rst
Normal file
5
docs/source/platypush/events/inotify.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.inotify``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.message.event.inotify
|
||||
:members:
|
5
docs/source/platypush/events/log.http.rst
Normal file
5
docs/source/platypush/events/log.http.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.log.http``
|
||||
====================================
|
||||
|
||||
.. automodule:: platypush.message.event.log.http
|
||||
:members:
|
5
docs/source/platypush/events/mail.rst
Normal file
5
docs/source/platypush/events/mail.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.mail``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.message.event.mail
|
||||
:members:
|
5
docs/source/platypush/events/nextcloud.rst
Normal file
5
docs/source/platypush/events/nextcloud.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.nextcloud``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.message.event.nextcloud
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``platypush.message.event.path``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.message.event.path
|
||||
:members:
|
||||
|
5
docs/source/platypush/events/zeroconf.rst
Normal file
5
docs/source/platypush/events/zeroconf.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.zeroconf``
|
||||
====================================
|
||||
|
||||
.. automodule:: platypush.message.event.zeroconf
|
||||
:members:
|
5
docs/source/platypush/plugins/camera.cv.rst
Normal file
5
docs/source/platypush/plugins/camera.cv.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.camera.cv``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.camera.cv
|
||||
:members:
|
5
docs/source/platypush/plugins/camera.ffmpeg.rst
Normal file
5
docs/source/platypush/plugins/camera.ffmpeg.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.camera.ffmpeg``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.plugins.camera.ffmpeg
|
||||
:members:
|
5
docs/source/platypush/plugins/camera.gstreamer.rst
Normal file
5
docs/source/platypush/plugins/camera.gstreamer.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.camera.gstreamer``
|
||||
======================================
|
||||
|
||||
.. automodule:: platypush.plugins.camera.gstreamer
|
||||
:members:
|
5
docs/source/platypush/plugins/config.rst
Normal file
5
docs/source/platypush/plugins/config.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.config``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.plugins.config
|
||||
:members:
|
5
docs/source/platypush/plugins/dbus.rst
Normal file
5
docs/source/platypush/plugins/dbus.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.dbus``
|
||||
==========================
|
||||
|
||||
.. automodule:: platypush.plugins.dbus
|
||||
:members:
|
5
docs/source/platypush/plugins/ffmpeg.rst
Normal file
5
docs/source/platypush/plugins/ffmpeg.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.ffmpeg``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.plugins.ffmpeg
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``platypush.plugins.google.credentials``
|
||||
========================================
|
||||
|
||||
.. automodule:: platypush.plugins.google.credentials
|
||||
:members:
|
||||
|
5
docs/source/platypush/plugins/gpio.sensor.dht.rst
Normal file
5
docs/source/platypush/plugins/gpio.sensor.dht.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.gpio.sensor.dht``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.plugins.gpio.sensor.dht
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.http.request.ota.booking``
|
||||
==============================================
|
||||
|
||||
.. automodule:: platypush.plugins.http.request.ota.booking
|
||||
:members:
|
5
docs/source/platypush/plugins/lcd.gpio.rst
Normal file
5
docs/source/platypush/plugins/lcd.gpio.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.lcd.gpio``
|
||||
==============================
|
||||
|
||||
.. automodule:: platypush.plugins.lcd.gpio
|
||||
:members:
|
5
docs/source/platypush/plugins/lcd.i2c.rst
Normal file
5
docs/source/platypush/plugins/lcd.i2c.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.lcd.i2c``
|
||||
=============================
|
||||
|
||||
.. automodule:: platypush.plugins.lcd.i2c
|
||||
:members:
|
5
docs/source/platypush/plugins/lcd.rst
Normal file
5
docs/source/platypush/plugins/lcd.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.lcd``
|
||||
=========================
|
||||
|
||||
.. automodule:: platypush.plugins.lcd
|
||||
:members:
|
5
docs/source/platypush/plugins/luma.oled.rst
Normal file
5
docs/source/platypush/plugins/luma.oled.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.luma.oled``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.luma.oled
|
||||
:members:
|
5
docs/source/platypush/plugins/mail.imap.rst
Normal file
5
docs/source/platypush/plugins/mail.imap.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.mail.imap``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.mail.imap
|
||||
:members:
|
5
docs/source/platypush/plugins/mail.rst
Normal file
5
docs/source/platypush/plugins/mail.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.mail``
|
||||
==========================
|
||||
|
||||
.. automodule:: platypush.plugins.mail
|
||||
:members:
|
5
docs/source/platypush/plugins/mail.smtp.rst
Normal file
5
docs/source/platypush/plugins/mail.smtp.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.mail.smtp``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.mail.smtp
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.media.ctrl``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.plugins.media.ctrl
|
||||
:members:
|
5
docs/source/platypush/plugins/media.gstreamer.rst
Normal file
5
docs/source/platypush/plugins/media.gstreamer.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.media.gstreamer``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.plugins.media.gstreamer
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``platypush.plugins.media.search.local``
|
||||
========================================
|
||||
|
||||
.. automodule:: platypush.plugins.media.search.local
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``platypush.plugins.media.search.torrent``
|
||||
==========================================
|
||||
|
||||
.. automodule:: platypush.plugins.media.search.torrent
|
||||
:members:
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
``platypush.plugins.media.search.youtube``
|
||||
==========================================
|
||||
|
||||
.. automodule:: platypush.plugins.media.search.youtube
|
||||
:members:
|
||||
|
5
docs/source/platypush/plugins/nextcloud.rst
Normal file
5
docs/source/platypush/plugins/nextcloud.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.nextcloud``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.nextcloud
|
||||
:members:
|
5
docs/source/platypush/plugins/pwm.pca9685.rst
Normal file
5
docs/source/platypush/plugins/pwm.pca9685.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.pwm.pca9685``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.plugins.pwm.pca9685
|
||||
:members:
|
5
docs/source/platypush/plugins/rtorrent.rst
Normal file
5
docs/source/platypush/plugins/rtorrent.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.rtorrent``
|
||||
==============================
|
||||
|
||||
.. automodule:: platypush.plugins.rtorrent
|
||||
:members:
|
5
docs/source/platypush/plugins/smartthings.rst
Normal file
5
docs/source/platypush/plugins/smartthings.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.smartthings``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.plugins.smartthings
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.stt.picovoice``
|
||||
===================================
|
||||
|
||||
.. automodule:: platypush.plugins.stt.picovoice
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
``platypush.plugins.switch.switchbot``
|
||||
======================================
|
||||
|
||||
.. automodule:: platypush.plugins.switch.switchbot
|
||||
:members:
|
||||
|
5
docs/source/platypush/plugins/switchbot.bluetooth.rst
Normal file
5
docs/source/platypush/plugins/switchbot.bluetooth.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.switchbot.bluetooth``
|
||||
=========================================
|
||||
|
||||
.. automodule:: platypush.plugins.switchbot.bluetooth
|
||||
:members:
|
5
docs/source/platypush/plugins/switchbot.rst
Normal file
5
docs/source/platypush/plugins/switchbot.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.switchbot``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.plugins.switchbot
|
||||
:members:
|
5
docs/source/platypush/plugins/twilio.rst
Normal file
5
docs/source/platypush/plugins/twilio.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.twilio``
|
||||
============================
|
||||
|
||||
.. automodule:: platypush.plugins.twilio
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.plugins.video.torrentcast``
|
||||
=======================================
|
||||
|
||||
.. automodule:: platypush.plugins.video.torrentcast
|
||||
:members:
|
5
docs/source/platypush/plugins/weather.openweathermap.rst
Normal file
5
docs/source/platypush/plugins/weather.openweathermap.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.weather.openweathermap``
|
||||
============================================
|
||||
|
||||
.. automodule:: platypush.plugins.weather.openweathermap
|
||||
:members:
|
5
docs/source/platypush/plugins/weather.rst
Normal file
5
docs/source/platypush/plugins/weather.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.weather``
|
||||
=============================
|
||||
|
||||
.. automodule:: platypush.plugins.weather
|
||||
:members:
|
5
docs/source/platypush/plugins/zeroconf.rst
Normal file
5
docs/source/platypush/plugins/zeroconf.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.zeroconf``
|
||||
==============================
|
||||
|
||||
.. automodule:: platypush.plugins.zeroconf
|
||||
:members:
|
5
docs/source/platypush/plugins/zwave._base.rst
Normal file
5
docs/source/platypush/plugins/zwave._base.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.zwave._base``
|
||||
=================================
|
||||
|
||||
.. automodule:: platypush.plugins.zwave._base
|
||||
:members:
|
5
docs/source/platypush/plugins/zwave.mqtt.rst
Normal file
5
docs/source/platypush/plugins/zwave.mqtt.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.plugins.zwave.mqtt``
|
||||
================================
|
||||
|
||||
.. automodule:: platypush.plugins.zwave.mqtt
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
``platypush.message.response.deepspeech``
|
||||
=========================================
|
||||
|
||||
.. automodule:: platypush.message.response.deepspeech
|
||||
:members:
|
|
@ -20,15 +20,21 @@ Plugins
|
|||
platypush/plugins/calendar.ical.rst
|
||||
platypush/plugins/camera.rst
|
||||
platypush/plugins/camera.android.ipcam.rst
|
||||
platypush/plugins/camera.cv.rst
|
||||
platypush/plugins/camera.ffmpeg.rst
|
||||
platypush/plugins/camera.gstreamer.rst
|
||||
platypush/plugins/camera.ir.mlx90640.rst
|
||||
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
|
||||
platypush/plugins/dbus.rst
|
||||
platypush/plugins/dropbox.rst
|
||||
platypush/plugins/esp.rst
|
||||
platypush/plugins/ffmpeg.rst
|
||||
platypush/plugins/file.rst
|
||||
platypush/plugins/foursquare.rst
|
||||
platypush/plugins/google.rst
|
||||
|
@ -44,6 +50,7 @@ Plugins
|
|||
platypush/plugins/gpio.sensor.rst
|
||||
platypush/plugins/gpio.sensor.accelerometer.rst
|
||||
platypush/plugins/gpio.sensor.bme280.rst
|
||||
platypush/plugins/gpio.sensor.dht.rst
|
||||
platypush/plugins/gpio.sensor.distance.rst
|
||||
platypush/plugins/gpio.sensor.distance.vl53l1x.rst
|
||||
platypush/plugins/gpio.sensor.envirophat.rst
|
||||
|
@ -54,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
|
||||
|
@ -62,13 +68,20 @@ Plugins
|
|||
platypush/plugins/inspect.rst
|
||||
platypush/plugins/kafka.rst
|
||||
platypush/plugins/lastfm.rst
|
||||
platypush/plugins/lcd.rst
|
||||
platypush/plugins/lcd.gpio.rst
|
||||
platypush/plugins/lcd.i2c.rst
|
||||
platypush/plugins/light.rst
|
||||
platypush/plugins/light.hue.rst
|
||||
platypush/plugins/linode.rst
|
||||
platypush/plugins/logger.rst
|
||||
platypush/plugins/luma.oled.rst
|
||||
platypush/plugins/mail.rst
|
||||
platypush/plugins/mail.imap.rst
|
||||
platypush/plugins/mail.smtp.rst
|
||||
platypush/plugins/media.rst
|
||||
platypush/plugins/media.chromecast.rst
|
||||
platypush/plugins/media.ctrl.rst
|
||||
platypush/plugins/media.gstreamer.rst
|
||||
platypush/plugins/media.kodi.rst
|
||||
platypush/plugins/media.mplayer.rst
|
||||
platypush/plugins/media.mpv.rst
|
||||
|
@ -84,17 +97,21 @@ Plugins
|
|||
platypush/plugins/music.rst
|
||||
platypush/plugins/music.mpd.rst
|
||||
platypush/plugins/music.snapcast.rst
|
||||
platypush/plugins/nextcloud.rst
|
||||
platypush/plugins/nmap.rst
|
||||
platypush/plugins/otp.rst
|
||||
platypush/plugins/pihole.rst
|
||||
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
|
||||
|
@ -102,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
|
||||
|
@ -115,14 +133,19 @@ Plugins
|
|||
platypush/plugins/tts.rst
|
||||
platypush/plugins/tts.google.rst
|
||||
platypush/plugins/tv.samsung.ws.rst
|
||||
platypush/plugins/twilio.rst
|
||||
platypush/plugins/udp.rst
|
||||
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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 6c0e65ccfe020bf6ce2eca43c387e827d4764c12
|
|
@ -16,21 +16,21 @@
|
|||
# Using multiple files is encouraged in the case of large configurations
|
||||
# that can easily end up in a messy config.yaml file, as they help you
|
||||
# keep your configuration more organized.
|
||||
include:
|
||||
- include/logging.yaml
|
||||
- include/media.yaml
|
||||
- include/sensors.yaml
|
||||
#include:
|
||||
# - include/logging.yaml
|
||||
# - include/media.yaml
|
||||
# - include/sensors.yaml
|
||||
|
||||
# platypush logs on stdout by default. You can use the logging section to specify
|
||||
# an alternative file or change the logging level.
|
||||
logging:
|
||||
filename: ~/.local/log/platypush/platypush.log
|
||||
level: INFO
|
||||
#logging:
|
||||
# filename: ~/.local/log/platypush/platypush.log
|
||||
# level: INFO
|
||||
|
||||
# The device_id is used by many components of platypush and it should uniquely
|
||||
# identify a device in your network. If nothing is specified then the hostname
|
||||
# will be used.
|
||||
device_id: myname
|
||||
#device_id: my_device
|
||||
|
||||
## --
|
||||
## Plugin configuration examples
|
||||
|
@ -40,10 +40,10 @@ device_id: myname
|
|||
# a plugin class. The methods of the class with @action annotation will
|
||||
# be exported as runnable actions, while the __init__ parameters are
|
||||
# configuration attributes that you can initialize in your config.yaml.
|
||||
# Plugin classes are documented at https://platypush.readthedocs.io/en/latest/plugins.html
|
||||
# Plugin classes are documented at https://docs.platypush.tech/en/latest/plugins.html
|
||||
#
|
||||
# In this example we'll configure the light.hue plugin, see
|
||||
# https://platypush.readthedocs.io/en/latest/platypush/plugins/light.hue.html
|
||||
# https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html
|
||||
# for reference. You can easily install the required dependencies for the plugin through
|
||||
# pip install 'platypush[hue]'
|
||||
light.hue:
|
||||
|
@ -54,14 +54,14 @@ light.hue:
|
|||
- Living Room
|
||||
|
||||
# Example configuration of music.mpd plugin, see
|
||||
# https://platypush.readthedocs.io/en/latest/platypush/plugins/music.mpd.html
|
||||
# https://docs.platypush.tech/en/latest/platypush/plugins/music.mpd.html
|
||||
# You can easily install the dependencies through pip install 'platypush[mpd]'
|
||||
music.mpd:
|
||||
host: localhost
|
||||
port: 6600
|
||||
|
||||
# Example configuration of media.chromecast plugin, see
|
||||
# https://platypush.readthedocs.io/en/latest/platypush/plugins/media.chromecast.html
|
||||
# https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html
|
||||
# You can easily install the dependencies through pip install 'platypush[chromecast]'
|
||||
media.chromecast:
|
||||
chromecast: Living Room TV
|
||||
|
@ -69,25 +69,16 @@ media.chromecast:
|
|||
# Plugins with empty configuration can also be explicitly enabled by specifying
|
||||
# enabled=True or disabled=False (it's a good practice if you want the
|
||||
# corresponding web panel to be enabled, if available)
|
||||
camera:
|
||||
camera.pi:
|
||||
enabled: True
|
||||
|
||||
# Support for last.fm scrobbling. Install dependencies with 'pip install "platypush[lastfm]"
|
||||
lastfm:
|
||||
api_key: your_api_key
|
||||
api_secret: your_api_secret
|
||||
username: your_username
|
||||
password: your_password
|
||||
|
||||
# Support for calendars - in this case Google and Facebook calendars
|
||||
# Installing the dependencies: pip install 'platypush[ical,google]'
|
||||
calendar:
|
||||
calendars:
|
||||
-
|
||||
type: platypush.plugins.google.calendar.GoogleCalendarPlugin
|
||||
-
|
||||
type: platypush.plugins.calendar.ical.CalendarIcalPlugin
|
||||
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
|
||||
- type: platypush.plugins.google.calendar.GoogleCalendarPlugin
|
||||
- type: platypush.plugins.calendar.ical.CalendarIcalPlugin
|
||||
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
|
||||
|
||||
## --
|
||||
## Backends configuration examples
|
||||
|
@ -97,10 +88,10 @@ calendar:
|
|||
# to happen and either trigger events or provide additional services on top of platypush.
|
||||
# Just like plugins, backends are classes whose configuration matches one-to-one the
|
||||
# supported parameters on the __init__ methods. You can check the documentation for the
|
||||
# available backends here: https://platypush.readthedocs.io/en/latest/backends.html.
|
||||
# available backends here: https://docs.platypush.tech/en/latest/backends.html.
|
||||
# Moreover, most of the backends will generate events that you can react to through custom
|
||||
# event hooks. Check here for the events documentation:
|
||||
# https://platypush.readthedocs.io/en/latest/events.html
|
||||
# https://docs.platypush.tech/en/latest/events.html
|
||||
#
|
||||
# You may usually want to enable the HTTP backend, as it provides many useful features on
|
||||
# top of platypush. Among those:
|
||||
|
@ -118,40 +109,14 @@ calendar:
|
|||
backend.http:
|
||||
# Listening port
|
||||
port: 8008
|
||||
# Websocket port
|
||||
websocket_port: 8009
|
||||
|
||||
# Through resource_dirs you can specify external folders whose content can be accessed on
|
||||
# the web server through a custom URL. In the case below we have a Dropbox folder containing
|
||||
# our pictures and we mount it to the '/carousel' endpoint.
|
||||
resource_dirs:
|
||||
carousel: ~/Dropbox/Photos/carousel
|
||||
|
||||
# Dashboard configuration. The dashboard is a collection of widgets and it's organized in
|
||||
# multiple rows. Each rows can be split in 12 columns. Therefore 'columns: 12' will make
|
||||
# a widget span over the whole row, while 'columns: 6' will make a widget take half the
|
||||
# horizontal space of a column.
|
||||
dashboard:
|
||||
widgets:
|
||||
-
|
||||
widget: calendar
|
||||
columns: 6
|
||||
-
|
||||
widget: music
|
||||
columns: 3
|
||||
-
|
||||
widget: date-time-weather
|
||||
columns: 3
|
||||
-
|
||||
widget: image-carousel
|
||||
columns: 6
|
||||
images_path: ~/Dropbox/Photos/carousel
|
||||
refresh_seconds: 15
|
||||
-
|
||||
widget: rss-news
|
||||
# Requires backend.http.poll to be enabled with some
|
||||
# RSS sources and write them to sqlite db
|
||||
columns: 6
|
||||
limit: 25
|
||||
db: "sqlite:////home/user/.local/share/platypush/feeds/rss.db"
|
||||
carousel: /mnt/hd/photos/carousel
|
||||
|
||||
# The HTTP poll backend is a versatile backend that can monitor for HTTP-based resources and
|
||||
# trigger events whenever new entries are available. In the example below we show how to use
|
||||
|
@ -160,35 +125,33 @@ backend.http:
|
|||
# Install the required dependencies through 'pip install "platypush[rss,db]"'
|
||||
backend.http.poll:
|
||||
requests:
|
||||
-
|
||||
# HTTP poll type (RSS)
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
# Remote URL
|
||||
url: http://www.theguardian.com/rss/world
|
||||
# Custom title
|
||||
title: The Guardian - World News
|
||||
# How often we should check for changes
|
||||
poll_seconds: 600
|
||||
# Maximum number of new entries to be processed
|
||||
max_entries: 10
|
||||
-
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://www.physorg.com/rss-feed
|
||||
title: Phys.org
|
||||
poll_seconds: 600
|
||||
max_entries: 10
|
||||
-
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://feeds.feedburner.com/Techcrunch
|
||||
title: Tech Crunch
|
||||
poll_seconds: 600
|
||||
max_entries: 10
|
||||
-
|
||||
type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||
title: The New York Times
|
||||
poll_seconds: 300
|
||||
max_entries: 10
|
||||
- type: platypush.backend.http.request.rss.RssUpdates # HTTP poll type (RSS)
|
||||
# Remote URL
|
||||
url: http://www.theguardian.com/rss/world
|
||||
# Custom title
|
||||
title: The Guardian - World News
|
||||
# How often we should check for changes
|
||||
poll_seconds: 600
|
||||
# Maximum number of new entries to be processed
|
||||
max_entries: 10
|
||||
|
||||
- type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://www.physorg.com/rss-feed
|
||||
title: Phys.org
|
||||
poll_seconds: 600
|
||||
max_entries: 10
|
||||
|
||||
- type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://feeds.feedburner.com/Techcrunch
|
||||
title: Tech Crunch
|
||||
poll_seconds: 600
|
||||
max_entries: 10
|
||||
|
||||
- type: platypush.backend.http.request.rss.RssUpdates
|
||||
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||
title: The New York Times
|
||||
poll_seconds: 300
|
||||
max_entries: 10
|
||||
|
||||
# MQTT backend. Installed required dependencies through 'pip install "platypush[mqtt]"'
|
||||
backend.mqtt:
|
||||
|
@ -196,15 +159,15 @@ backend.mqtt:
|
|||
host: mqtt-server
|
||||
# By default the backend will listen for messages on the platypush_bus_mq/device_id
|
||||
# topic, but you can change the prefix using the topic attribute
|
||||
topic: my_platypush_bus
|
||||
# topic: MyBus
|
||||
|
||||
# Raw TCP socket backend. It can run commands sent as JSON over telnet or netcat
|
||||
backend.tcp:
|
||||
port: 3333
|
||||
#backend.tcp:
|
||||
# port: 3333
|
||||
|
||||
# Websocket backend. Install required dependencies through 'pip install "platypush[http]"'
|
||||
backend.websocket:
|
||||
port: 8765
|
||||
#backend.websocket:
|
||||
# port: 8765
|
||||
|
||||
## --
|
||||
## Assistant configuration examples
|
||||
|
@ -254,9 +217,12 @@ backend.assistant.snowboy:
|
|||
assistant.echo:
|
||||
audio_player: mplayer
|
||||
|
||||
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant]"'
|
||||
assistant.google.pushtotalk:
|
||||
language: en-US
|
||||
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant-legacy]"'
|
||||
assistant.google:
|
||||
enabled: True
|
||||
|
||||
backend.assistant.google:
|
||||
enabled: True
|
||||
|
||||
## --
|
||||
## Procedure examples
|
||||
|
@ -327,14 +293,14 @@ procedure.outside_home:
|
|||
procedure.send_request(target, action, args):
|
||||
- action: mqtt.send_message
|
||||
args:
|
||||
topic: my_platypush_bus/${target}
|
||||
topic: platypush_bus_mq/${target}
|
||||
host: mqtt-server
|
||||
port: 1883
|
||||
msg:
|
||||
type: request
|
||||
target: ${target}
|
||||
action: ${action}
|
||||
args: "${context.get('args', {}}"
|
||||
args: ${args}
|
||||
|
||||
## --
|
||||
## Event hook examples
|
||||
|
@ -379,10 +345,8 @@ event.hook.SearchSongVoiceCommand:
|
|||
- action: music.mpd.search
|
||||
args:
|
||||
filter:
|
||||
- artist
|
||||
- ${artist}
|
||||
- any
|
||||
- ${title}
|
||||
artist: ${artist}
|
||||
title: ${title}
|
||||
|
||||
# Play the first search result
|
||||
- action: music.mpd.play
|
||||
|
|
33
examples/conf/dashboard.xml
Normal file
33
examples/conf/dashboard.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<!-- Dashboard templates are stored as ~/.config/platypush/dashboards/<name>.xml and can be accessed on
|
||||
http://<host>:8008/dashboard/<name>. A dashboard can show a custom set of widgets on a screen - e.g. calendar
|
||||
events, media information, photo carousels, sensors data, weather forecast and news headlines. The available
|
||||
widgets are stored as Vue.js templates under `platypush/backend/http/webapp/src/components/widgets`. -->
|
||||
<Dashboard>
|
||||
<!-- Display the following widgets on the same row. Each row consists of 12 columns.
|
||||
You can specify the width of each widget either through class name (e.g. col-6 means
|
||||
6 columns out of 12, e.g. half the size of the row) or inline style
|
||||
(e.g. `style="width: 50%"`). -->
|
||||
<Row>
|
||||
<!-- Show a calendar widget with the upcoming events. It requires the `calendar` plugin to
|
||||
be enabled and configured. -->
|
||||
<Calendar class="col-6" />
|
||||
|
||||
<!-- Show the current track and other playback info. It requires `music.mpd` plugin or any
|
||||
other music plugin enabled. -->
|
||||
<Music class="col-3" />
|
||||
|
||||
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
|
||||
<DateTimeWeather class="col-3" />
|
||||
</Row>
|
||||
|
||||
<!-- Display the following widgets on a second row -->
|
||||
<Row>
|
||||
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
||||
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
|
||||
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
||||
|
||||
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
||||
`http.poll` backend -->
|
||||
<RssNews class="col-6" db="sqlite:////path/to/your/rss.db" />
|
||||
</Row>
|
||||
</Dashboard>
|
43
examples/conf/hook.py
Normal file
43
examples/conf/hook.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# A more versatile way to define event hooks than the YAML format of `config.yaml` is through native Python scripts.
|
||||
# You can define hooks as simple Python functions that use the `platypush.event.hook.hook` decorator to specify on
|
||||
# which event type they should be called, and optionally on which event attribute values.
|
||||
#
|
||||
# Event hooks should be stored in Python files under `~/.config/platypush/scripts`. All the functions that use the
|
||||
# @hook decorator will automatically be discovered and imported as event hooks into the platform at runtime.
|
||||
|
||||
# `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
|
||||
from platypush.utils import run
|
||||
|
||||
# @hook decorator
|
||||
from platypush.event.hook import hook
|
||||
|
||||
# Event types that you want to react to
|
||||
from platypush.message.event.assistant import ConversationStartEvent, SpeechRecognizedEvent
|
||||
|
||||
|
||||
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
|
||||
def on_music_play_command(event, title=None, artist=None, **context):
|
||||
"""
|
||||
This function will be executed when a SpeechRecognizedEvent with `phrase="play the music"` is triggered.
|
||||
`event` contains the event object and `context` any key-value info from the running context.
|
||||
Note that in this specific case we can leverage the token-extraction feature of SpeechRecognizedEvent through
|
||||
${} that operates on regex-like principles to extract any text that matches the pattern into context variables.
|
||||
"""
|
||||
results = run('music.mpd.search', filter={
|
||||
'artist': artist,
|
||||
'title': title,
|
||||
})
|
||||
|
||||
if results:
|
||||
run('music.mpd.play', results[0]['file'])
|
||||
else:
|
||||
run('tts.say', "I can't find any music matching your query")
|
||||
|
||||
|
||||
@hook(ConversationStartEvent)
|
||||
def on_conversation_start(event, **context):
|
||||
"""
|
||||
A simple hook that gets invoked when a new conversation starts with a voice assistant and simply pauses the music
|
||||
to make sure that your speech is properly detected.
|
||||
"""
|
||||
run('music.mpd.pause_if_playing')
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
[Unit]
|
||||
Description=Platypush daemon
|
||||
After=network.target
|
||||
After=network.target redis.service
|
||||
|
||||
[Service]
|
||||
# platypush installation path
|
||||
|
|
|
@ -16,18 +16,17 @@ from .context import register_backends
|
|||
from .cron.scheduler import CronScheduler
|
||||
from .event.processor import EventProcessor
|
||||
from .logger import Logger
|
||||
from .message.event import Event, StopEvent
|
||||
from .message.event.application import ApplicationStartedEvent, ApplicationStoppedEvent
|
||||
from .message.event import Event
|
||||
from .message.event.application import ApplicationStartedEvent
|
||||
from .message.request import Request
|
||||
from .message.response import Response
|
||||
from .utils import set_thread_name
|
||||
|
||||
|
||||
__author__ = 'Fabio Manganiello <blacklight86@gmail.com>'
|
||||
__version__ = '0.12.10'
|
||||
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
||||
__version__ = '0.21.1'
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
logger = logging.getLogger('platypush')
|
||||
|
||||
|
||||
class Daemon:
|
||||
|
@ -42,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
|
||||
|
@ -51,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:
|
||||
|
@ -65,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:
|
||||
|
@ -72,15 +75,21 @@ 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(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()
|
||||
self.requests_to_process = requests_to_process
|
||||
self.processed_requests = 0
|
||||
self.cron_scheduler = None
|
||||
|
||||
@classmethod
|
||||
def build_from_cmdline(cls, args):
|
||||
|
@ -106,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):
|
||||
"""
|
||||
|
@ -128,20 +143,17 @@ class Daemon:
|
|||
try:
|
||||
msg.execute(n_tries=self.n_tries)
|
||||
except PermissionError:
|
||||
LOGGER.info('Dropped unauthorized request: {}'.format(msg))
|
||||
logger.info('Dropped unauthorized request: {}'.format(msg))
|
||||
|
||||
self.processed_requests += 1
|
||||
if self.requests_to_process \
|
||||
and self.processed_requests >= self.requests_to_process:
|
||||
self.stop_app()
|
||||
elif isinstance(msg, Response):
|
||||
LOGGER.info('Received response: {}'.format(msg))
|
||||
elif isinstance(msg, StopEvent) and msg.targets_me():
|
||||
LOGGER.info('Received STOP event: {}'.format(msg))
|
||||
self.stop_app()
|
||||
logger.info('Received response: {}'.format(msg))
|
||||
elif isinstance(msg, Event):
|
||||
if not msg.disable_logging:
|
||||
LOGGER.info('Received event: {}'.format(msg))
|
||||
logger.info('Received event: {}'.format(msg))
|
||||
self.event_processor.process_event(msg)
|
||||
|
||||
return _f
|
||||
|
@ -150,22 +162,20 @@ class Daemon:
|
|||
""" Stops the backends and the bus """
|
||||
for backend in self.backends.values():
|
||||
backend.stop()
|
||||
self.bus.stop()
|
||||
|
||||
def start(self):
|
||||
self.bus.stop()
|
||||
if self.cron_scheduler:
|
||||
self.cron_scheduler.stop()
|
||||
|
||||
def run(self):
|
||||
""" Start the daemon """
|
||||
if not self.no_capture_stdout:
|
||||
sys.stdout = Logger(LOGGER.info)
|
||||
sys.stdout = Logger(logger.info)
|
||||
if not self.no_capture_stderr:
|
||||
sys.stderr = Logger(LOGGER.warning)
|
||||
sys.stderr = Logger(logger.warning)
|
||||
|
||||
set_thread_name('platypush')
|
||||
|
||||
print('---- Starting platypush v.{}'.format(__version__))
|
||||
|
||||
redis_conf = Config.get('backend.redis') or {}
|
||||
self.bus = RedisBus(on_message=self.on_message(),
|
||||
**redis_conf.get('redis_args', {}))
|
||||
logger.info('---- Starting platypush v.{}'.format(__version__))
|
||||
|
||||
# Initialize the backends and link them to the bus
|
||||
self.backends = register_backends(bus=self.bus, global_scope=True)
|
||||
|
@ -176,7 +186,8 @@ class Daemon:
|
|||
|
||||
# Start the cron scheduler
|
||||
if Config.get_cronjobs():
|
||||
CronScheduler(jobs=Config.get_cronjobs()).start()
|
||||
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())
|
||||
self.cron_scheduler.start()
|
||||
|
||||
self.bus.post(ApplicationStartedEvent())
|
||||
|
||||
|
@ -184,9 +195,8 @@ class Daemon:
|
|||
try:
|
||||
self.bus.poll()
|
||||
except KeyboardInterrupt:
|
||||
LOGGER.info('SIGINT received, terminating application')
|
||||
logger.info('SIGINT received, terminating application')
|
||||
finally:
|
||||
self.bus.post(ApplicationStoppedEvent())
|
||||
self.stop_app()
|
||||
|
||||
|
||||
|
@ -194,9 +204,8 @@ def main():
|
|||
"""
|
||||
Platypush daemon main
|
||||
"""
|
||||
|
||||
app = Daemon.build_from_cmdline(sys.argv[1:])
|
||||
app.start()
|
||||
app.run()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -4,21 +4,24 @@
|
|||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import re
|
||||
import socket
|
||||
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
|
||||
from platypush.context import get_backend
|
||||
from platypush.message.event.zeroconf import ZeroconfServiceAddedEvent, ZeroconfServiceRemovedEvent
|
||||
from platypush.utils import set_timeout, clear_timeout, \
|
||||
get_redis_queue_name_by_message, set_thread_name
|
||||
get_redis_queue_name_by_message, set_thread_name, get_backend_name_by_class
|
||||
|
||||
from platypush import __version__
|
||||
from platypush.event import EventGenerator
|
||||
from platypush.message import Message
|
||||
from platypush.message.event import Event, StopEvent
|
||||
from platypush.message.event import Event
|
||||
from platypush.message.request import Request
|
||||
from platypush.message.response import Response
|
||||
|
||||
|
@ -50,7 +53,7 @@ class Backend(Thread, EventGenerator):
|
|||
|
||||
self._thread_name = self.__class__.__name__
|
||||
EventGenerator.__init__(self)
|
||||
Thread.__init__(self, name=self._thread_name)
|
||||
Thread.__init__(self, name=self._thread_name, daemon=True)
|
||||
|
||||
# If no bus is specified, create an internal queue where
|
||||
# the received messages will be pushed
|
||||
|
@ -58,10 +61,11 @@ 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._should_stop = False
|
||||
self._stop_event = threading.Event()
|
||||
self._stop_event = ThreadEvent()
|
||||
self._kwargs = kwargs
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
|
||||
self.zeroconf = None
|
||||
self.zeroconf_info = None
|
||||
|
||||
# Internal-only, we set the request context on a backend if that
|
||||
# backend is intended to react for a response to a specific request
|
||||
|
@ -71,8 +75,6 @@ class Backend(Thread, EventGenerator):
|
|||
if 'logging' in kwargs:
|
||||
self.logger.setLevel(getattr(logging, kwargs.get('logging').upper()))
|
||||
|
||||
Thread.__init__(self)
|
||||
|
||||
def on_message(self, msg):
|
||||
"""
|
||||
Callback when a message is received on the backend.
|
||||
|
@ -99,12 +101,8 @@ class Backend(Thread, EventGenerator):
|
|||
self.stop()
|
||||
return
|
||||
|
||||
if isinstance(msg, StopEvent) and msg.targets_me():
|
||||
self.logger.info('Received STOP event on {}'.format(self.__class__.__name__))
|
||||
self._should_stop = True
|
||||
else:
|
||||
msg.backend = self # Augment message to be able to process responses
|
||||
self.bus.post(msg)
|
||||
msg.backend = self # Augment message to be able to process responses
|
||||
self.bus.post(msg)
|
||||
|
||||
def _is_expected_response(self, msg):
|
||||
""" Internal only - returns true if we are expecting for a response
|
||||
|
@ -221,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
|
||||
|
@ -264,20 +262,17 @@ class Backend(Thread, EventGenerator):
|
|||
def stop(self):
|
||||
""" Stops the backend thread by sending a STOP event on its bus """
|
||||
def _async_stop():
|
||||
evt = StopEvent(target=self.device_id, origin=self.device_id,
|
||||
thread_id=self.thread_id)
|
||||
|
||||
self.send_message(evt)
|
||||
self._stop_event.set()
|
||||
self.unregister_service()
|
||||
self.on_stop()
|
||||
|
||||
Thread(target=_async_stop).start()
|
||||
|
||||
def should_stop(self):
|
||||
return self._should_stop
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def wait_stop(self, timeout=None):
|
||||
self._stop_event.wait(timeout)
|
||||
def wait_stop(self, timeout=None) -> bool:
|
||||
return self._stop_event.wait(timeout)
|
||||
|
||||
def _get_redis(self):
|
||||
import redis
|
||||
|
@ -306,5 +301,104 @@ class Backend(Thread, EventGenerator):
|
|||
except Exception as e:
|
||||
self.logger.error('Error while processing response to {}: {}'.format(msg, str(e)))
|
||||
|
||||
@staticmethod
|
||||
def _get_ip() -> str:
|
||||
"""
|
||||
Get the IP address of the machine.
|
||||
"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(('8.8.8.8', 80))
|
||||
addr = s.getsockname()[0]
|
||||
s.close()
|
||||
return addr
|
||||
|
||||
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
|
||||
from platypush.plugins.zeroconf import ZeroconfListener
|
||||
except ImportError:
|
||||
self.logger.warning('zeroconf package not available, service discovery will be disabled.')
|
||||
return
|
||||
|
||||
self.zeroconf = Zeroconf()
|
||||
srv_desc = {
|
||||
'name': 'Platypush',
|
||||
'vendor': 'Platypush',
|
||||
'version': __version__,
|
||||
**(properties or {}),
|
||||
}
|
||||
|
||||
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
|
||||
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
|
||||
else:
|
||||
srv_port = self.port if hasattr(self, 'port') else None
|
||||
|
||||
self.zeroconf_info = ServiceInfo(srv_type, srv_name,
|
||||
addresses=[socket.inet_aton(self._get_ip())],
|
||||
port=srv_port,
|
||||
weight=0,
|
||||
priority=0,
|
||||
properties=srv_desc)
|
||||
|
||||
if not self.zeroconf_info:
|
||||
self.logger.warning('Could not register Zeroconf service')
|
||||
return
|
||||
|
||||
self.zeroconf.register_service(self.zeroconf_info)
|
||||
self.bus.post(ZeroconfServiceAddedEvent(service_type=srv_type, service_name=srv_name,
|
||||
service_info=ZeroconfListener.parse_service_info(self.zeroconf_info)))
|
||||
|
||||
def unregister_service(self):
|
||||
"""
|
||||
Unregister the Zeroconf service configuration if available.
|
||||
"""
|
||||
if self.zeroconf and self.zeroconf_info:
|
||||
try:
|
||||
self.zeroconf.unregister_service(self.zeroconf_info)
|
||||
except Exception as e:
|
||||
self.logger.warning('Could not register Zeroconf service {}: {}: {}'.format(
|
||||
self.zeroconf_info.name, type(e).__name__, str(e)))
|
||||
|
||||
if self.zeroconf:
|
||||
self.zeroconf.close()
|
||||
|
||||
if self.zeroconf_info:
|
||||
self.bus.post(ZeroconfServiceRemovedEvent(service_type=self.zeroconf_info.type,
|
||||
service_name=self.zeroconf_info.name))
|
||||
else:
|
||||
self.bus.post(ZeroconfServiceRemovedEvent(service_type=None, service_name=None))
|
||||
|
||||
self.zeroconf_info = None
|
||||
self.zeroconf = None
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -42,6 +42,7 @@ class AdafruitIoBackend(Backend):
|
|||
if not plugin:
|
||||
raise RuntimeError('Adafruit IO plugin not configured')
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
self._client = MQTTClient(plugin._username, plugin._key)
|
||||
self._client.on_connect = self.on_connect()
|
||||
self._client.on_disconnect = self.on_disconnect()
|
||||
|
@ -52,18 +53,25 @@ class AdafruitIoBackend(Backend):
|
|||
for feed in self.feeds:
|
||||
client.subscribe(feed)
|
||||
self.bus.post(ConnectedEvent())
|
||||
|
||||
return _handler
|
||||
|
||||
def on_disconnect(self):
|
||||
def _handler(client):
|
||||
self.bus.post(DisconnectedEvent())
|
||||
|
||||
return _handler
|
||||
|
||||
def on_message(self):
|
||||
def on_message(self, msg):
|
||||
# noinspection PyUnusedLocal
|
||||
def _handler(client, feed, data):
|
||||
try: data = float(data)
|
||||
except: pass
|
||||
try:
|
||||
data = float(data)
|
||||
except Exception as e:
|
||||
self.logger.debug('Not a number: {}: {}'.format(data, e))
|
||||
|
||||
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
|
||||
|
||||
return _handler
|
||||
|
||||
def run(self):
|
||||
|
@ -81,5 +89,4 @@ class AdafruitIoBackend(Backend):
|
|||
self.logger.exception(e)
|
||||
self._client = None
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
import enum
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
|
@ -6,7 +7,7 @@ import threading
|
|||
from typing import Optional, Union, Dict, Any, List
|
||||
|
||||
import croniter
|
||||
import enum
|
||||
from dateutil.tz import gettz
|
||||
|
||||
from platypush.backend import Backend
|
||||
from platypush.context import get_bus, get_plugin
|
||||
|
@ -54,18 +55,20 @@ class Alarm:
|
|||
self._runtime_snooze_interval = snooze_interval
|
||||
|
||||
def get_next(self) -> float:
|
||||
now = time.time()
|
||||
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).timestamp()
|
||||
timestamp = datetime.datetime.fromisoformat(self.when).replace(
|
||||
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
||||
except (TypeError, ValueError):
|
||||
timestamp = (datetime.datetime.now() + datetime.timedelta(seconds=int(self.when))).timestamp()
|
||||
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable]
|
||||
datetime.timedelta(seconds=int(self.when)))
|
||||
|
||||
return timestamp if timestamp >= now else None
|
||||
return timestamp.timestamp() if timestamp >= now else None
|
||||
|
||||
def is_enabled(self):
|
||||
return self._enabled
|
||||
|
|
|
@ -21,12 +21,10 @@ class AssistantGoogleBackend(AssistantBackend):
|
|||
|
||||
It listens for voice commands and post conversation events on the bus.
|
||||
|
||||
**WARNING**: This backend is deprecated, as the underlying Google Assistant
|
||||
library has been deprecated too: https://developers.google.com/assistant/sdk/reference/library/python/
|
||||
The old library might still work on some systems but its proper functioning
|
||||
is not guaranteed.
|
||||
Please use the Snowboy backend for hotword detection and the Google Assistant
|
||||
push-to-talk plugin for assistant interaction instead.
|
||||
**WARNING**: The Google Assistant library used by this backend has officially been deprecated:
|
||||
https://developers.google.com/assistant/sdk/reference/library/python/. This backend still works on most of the
|
||||
devices where I use it, but its correct functioning is not guaranteed as the assistant library is no longer
|
||||
maintained.
|
||||
|
||||
Triggers:
|
||||
|
||||
|
|
|
@ -165,6 +165,7 @@ class AssistantSnowboyBackend(AssistantBackend):
|
|||
return callback
|
||||
|
||||
def on_stop(self):
|
||||
super().on_stop()
|
||||
if self.detector:
|
||||
self.detector.terminate()
|
||||
self.detector = None
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -19,6 +19,10 @@ class CameraPiBackend(Backend):
|
|||
|
||||
* **picamera** (``pip install picamera``)
|
||||
* **redis** (``pip install redis``) for inter-process communication with the camera process
|
||||
|
||||
This backend is **DEPRECATED**. Use the plugin :class:`platypush.plugins.camera.pi.CameraPiPlugin` instead to run
|
||||
Pi camera actions. If you want to start streaming the camera on application start then simply create an event hook
|
||||
on :class:`platypush.message.event.application.ApplicationStartedEvent` that runs ``camera.pi.start_streaming``.
|
||||
"""
|
||||
|
||||
class CameraAction(Enum):
|
||||
|
@ -30,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,
|
||||
|
@ -45,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
|
||||
|
@ -118,7 +126,7 @@ class CameraPiBackend(Backend):
|
|||
while True:
|
||||
self.camera.wait_recording(2)
|
||||
else:
|
||||
while True:
|
||||
while not self.should_stop():
|
||||
connection = self.server_socket.accept()[0].makefile('wb')
|
||||
self.logger.info('Accepted client connection on port {}'.format(self.listen_port))
|
||||
|
||||
|
@ -130,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)
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ class ClipboardBackend(Backend):
|
|||
self._last_text: Optional[str] = None
|
||||
|
||||
def run(self):
|
||||
self.logger.info('Started clipboard monitor backend')
|
||||
while not self.should_stop():
|
||||
text = pyperclip.paste()
|
||||
if text and text != self._last_text:
|
||||
|
@ -34,5 +35,6 @@ class ClipboardBackend(Backend):
|
|||
self._last_text = text
|
||||
time.sleep(0.1)
|
||||
|
||||
self.logger.info('Stopped clipboard monitor backend')
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
86
platypush/backend/dbus.py
Normal file
86
platypush/backend/dbus.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
from typing import Union
|
||||
|
||||
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
||||
from gi.repository import GLib
|
||||
|
||||
import dbus
|
||||
import dbus.service
|
||||
import dbus.mainloop.glib
|
||||
|
||||
from platypush.backend import Backend
|
||||
from platypush.context import get_bus
|
||||
from platypush.message import Message
|
||||
from platypush.message.event import Event
|
||||
from platypush.message.request import Request
|
||||
from platypush.utils import run
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class DBusService(dbus.service.Object):
|
||||
@classmethod
|
||||
def _parse_msg(cls, msg: Union[dict, list]):
|
||||
import json
|
||||
return Message.build(json.loads(json.dumps(msg)))
|
||||
|
||||
@dbus.service.method('org.platypush.MessageBusInterface', in_signature='a{sv}', out_signature='v')
|
||||
def Post(self, msg: dict):
|
||||
"""
|
||||
This method accepts a message as a dictionary (either representing a valid request or an event) and either
|
||||
executes it (request) or forwards it to the application bus (event).
|
||||
|
||||
:param msg: Request or event, as a dictionary.
|
||||
:return: The return value of the request, or 0 if the message is an event.
|
||||
"""
|
||||
msg = self._parse_msg(msg)
|
||||
if isinstance(msg, Request):
|
||||
ret = run(msg.action, **msg.args)
|
||||
if ret is None:
|
||||
ret = '' # DBus doesn't like None return types
|
||||
|
||||
return ret
|
||||
elif isinstance(msg, Event):
|
||||
get_bus().post(msg)
|
||||
return 0
|
||||
|
||||
|
||||
class DbusBackend(Backend):
|
||||
"""
|
||||
This backend acts as a proxy that receives messages (requests or events) on the DBus and forwards them to the
|
||||
application bus.
|
||||
|
||||
The name of the messaging interface exposed by Platypush is ``org.platypush.MessageBusInterface`` and it exposes
|
||||
``Post`` method, which accepts a dictionary representing a valid Platypush message (either a request or an event)
|
||||
and either executes it or forwards it to the application bus.
|
||||
|
||||
Requires:
|
||||
|
||||
* **dbus-python** (``pip install dbus-python``)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bus_name='org.platypush.Bus', service_path='/MessageService', *args, **kwargs):
|
||||
"""
|
||||
:param bus_name: Name of the bus where the application will listen for incoming messages (default:
|
||||
``org.platypush.Bus``).
|
||||
:param service_path: Path to the service exposed by the app (default: ``/MessageService``).
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.bus_name = bus_name
|
||||
self.service_path = service_path
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
bus = dbus.SessionBus()
|
||||
name = dbus.service.BusName(self.bus_name, bus)
|
||||
srv = DBusService(bus, self.service_path)
|
||||
|
||||
loop = GLib.MainLoop()
|
||||
# noinspection PyProtectedMember
|
||||
self.logger.info('Starting DBus main loop - bus name: {}, service: {}'.format(name._name, srv._object_path))
|
||||
loop.run()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue