forked from platypush/platypush
Compare commits
164 Commits
Author | SHA1 | Date |
---|---|---|
Fabio Manganiello | 99de5318ff | |
Fabio Manganiello | b3bab9b1d8 | |
Fabio Manganiello | 7e4877c793 | |
Fabio Manganiello | 55602cc282 | |
Fabio Manganiello | d2053a012a | |
snyk-bot | 3d5fc9a10b | |
snyk-bot | be4dd48d76 | |
snyk-bot | bd21779a17 | |
snyk-bot | 58afc1090c | |
Fabio Manganiello | 7c87238fec | |
Fabio Manganiello | cbf8ba7fdd | |
Fabio Manganiello | c6c7128099 | |
Fabio Manganiello | 1a316bc3a6 | |
Fabio Manganiello | 5966566d54 | |
Fabio Manganiello | 8d26c8634d | |
Fabio Manganiello | 1da17fca35 | |
Fabio Manganiello | 35cb72f5aa | |
Fabio Manganiello | 115bed7d8b | |
Fabio Manganiello | 3d22d6b082 | |
Fabio Manganiello | 5971ec32c8 | |
Fabio Manganiello | 7e31ac6ed8 | |
Fabio Manganiello | c9f435a6cb | |
Fabio Manganiello | cb7021152f | |
Fabio Manganiello | d3f4865395 | |
Fabio Manganiello | f080478385 | |
Fabio Manganiello | b857ce60a7 | |
Fabio Manganiello | af483cade3 | |
Fabio Manganiello | 8be515c17b | |
Fabio Manganiello | 239025290d | |
Fabio Manganiello | 2e2169544d | |
Fabio Manganiello | 7a0e39111d | |
Fabio Manganiello | 2cd7ae9513 | |
Fabio Manganiello | 55958c1b57 | |
Fabio Manganiello | e9454b0c0f | |
Fabio Manganiello | ba23eb7280 | |
Fabio Manganiello | 41d0725ebf | |
Fabio Manganiello | 820a1c8184 | |
dependabot[bot] | 5929602c15 | |
Fabio Manganiello | fee5fc4ae0 | |
Fabio Manganiello | 371fd7e46b | |
Fabio Manganiello | da73a5f1b9 | |
Fabio Manganiello | a80adc996f | |
Fabio Manganiello | 12887b61fe | |
Fabio Manganiello | ca25607262 | |
Fabio Manganiello | 1b30bfc454 | |
Fabio Manganiello | 486801653a | |
Fabio Manganiello | f7c594cc3f | |
Fabio Manganiello | b1491b8048 | |
Fabio Manganiello | 96a2d8bef0 | |
Fabio Manganiello | e261dcc27a | |
Fabio Manganiello | d0790aaba3 | |
Fabio Manganiello | bb28617cc9 | |
Fabio Manganiello | e1e6da9307 | |
Fabio Manganiello | f6ce0d7200 | |
Fabio Manganiello | ed5f7070a2 | |
Fabio Manganiello | 5ee47902f4 | |
Fabio Manganiello | 128b45686a | |
Fabio Manganiello | 3d192a9733 | |
Fabio Manganiello | 08acaad218 | |
Fabio Manganiello | 385914c04a | |
dependabot[bot] | b72c9a19ae | |
Fabio Manganiello | 2e33d3b3c5 | |
Fabio Manganiello | c5395cc9e5 | |
Fabio Manganiello | 88846aa8f8 | |
Fabio Manganiello | ffd23cf04d | |
Fabio Manganiello | 34e1e673e8 | |
Fabio Manganiello | c3934e2a7e | |
Fabio Manganiello | b3b2a7a805 | |
Fabio Manganiello | 747e7f3e5d | |
Fabio Manganiello | fdf6d8fb4e | |
Fabio Manganiello | 7c9e9d284d | |
Fabio Manganiello | 19a1e9c626 | |
Fabio Manganiello | c0039c3f87 | |
Fabio Manganiello | 0d0797a465 | |
Fabio Manganiello | 0b293ff214 | |
Fabio Manganiello | 75ad537155 | |
dependabot[bot] | 0324eb9f6b | |
Fabio Manganiello | e3f67766a3 | |
Fabio Manganiello | 1933ec709f | |
Fabio Manganiello | 71cb73cf63 | |
Fabio Manganiello | 94bb3e0541 | |
Fabio Manganiello | 29a7eff15a | |
Fabio Manganiello | d13e4fc271 | |
Fabio Manganiello | 6e0c249b7e | |
Fabio Manganiello | 2944a77f93 | |
dependabot[bot] | 5b666814d5 | |
dependabot[bot] | 21ad599a08 | |
Fabio Manganiello | 782c198311 | |
Fabio Manganiello | 08b3cddb7b | |
Fabio Manganiello | 530245733c | |
dependabot[bot] | 1662873e54 | |
Fabio Manganiello | 42ee149b95 | |
Fabio Manganiello | 1038090ffd | |
Fabio Manganiello | 786286eac6 | |
Fabio Manganiello | 1914322fda | |
Fabio Manganiello | e4eb12fa6d | |
Fabio Manganiello | c534adf31f | |
Fabio Manganiello | 0c423e3809 | |
Fabio Manganiello | 6656bb4ce5 | |
Fabio Manganiello | f3be4a50d8 | |
Fabio Manganiello | a6b552504e | |
Fabio Manganiello | 833e1c49be | |
Fabio Manganiello | a46ce79f0a | |
Fabio Manganiello | e9f6d9a8bc | |
Fabio Manganiello | 3e4b91cd6c | |
Fabio Manganiello | e242dc53bf | |
Fabio Manganiello | ee0b6d237a | |
Fabio Manganiello | 9ba2c18595 | |
Fabio Manganiello | 0a3fd4065a | |
Fabio Manganiello | e94d338de5 | |
Fabio Manganiello | 081da3eb84 | |
Fabio Manganiello | 1569f940c6 | |
Fabio Manganiello | 6df9cbcf3c | |
Fabio Manganiello | e98aa63110 | |
Fabio Manganiello | fa0f4925ed | |
Fabio Manganiello | fa708663e1 | |
Fabio Manganiello | 20fc3d91fc | |
Fabio Manganiello | 2560bfa03f | |
Fabio Manganiello | 46d8d575ba | |
Fabio Manganiello | f478e1ff40 | |
Fabio Manganiello | 6023fd3db3 | |
Fabio Manganiello | f6057274a0 | |
Fabio Manganiello | 2d9dff7d4c | |
Fabio Manganiello | f74ca28382 | |
Fabio Manganiello | e615891bf3 | |
Fabio Manganiello | 02b5ec1d38 | |
Fabio Manganiello | 2914a74b75 | |
Fabio Manganiello | 1e1bf46f32 | |
Fabio Manganiello | 848b736d6e | |
Fabio Manganiello | f9f9c38a8b | |
Fabio Manganiello | 518d9f20c6 | |
Fabio Manganiello | 40903393df | |
Fabio Manganiello | d5cddc23fa | |
Fabio Manganiello | ea3b49a17f | |
Fabio Manganiello | adb9672989 | |
Fabio Manganiello | b432488876 | |
Fabio Manganiello | 65d4740cd7 | |
Fabio Manganiello | 6ba3128ac4 | |
Fabio Manganiello | 82fe4d93f3 | |
Fabio Manganiello | d7b273434b | |
dependabot[bot] | 5491682543 | |
Fabio Manganiello | 3ef602cc0b | |
Fabio Manganiello | 195ae5c488 | |
Fabio Manganiello | de25719563 | |
Fabio Manganiello | e86c61a44d | |
Fabio Manganiello | acdc636b1f | |
Fabio Manganiello | 6db070db1c | |
Fabio Manganiello | 952a2a9379 | |
Fabio Manganiello | 49676fcc7f | |
Fabio Manganiello | 6c0a8bf259 | |
Fabio Manganiello | 1906876969 | |
Fabio Manganiello | f9ce03919b | |
Fabio Manganiello | c3681e7b2a | |
Fabio Manganiello | 144700b693 | |
Fabio Manganiello | 4a5bb766af | |
Fabio Manganiello | cb4cfa40d8 | |
Fabio Manganiello | 8c339d0d55 | |
Fabio Manganiello | 1962a8c4de | |
Fabio Manganiello | c664796733 | |
Fabio Manganiello | 64c402b1c0 | |
Fabio Manganiello | a5f1dc2638 | |
Fabio Manganiello | 0e27c8f68a | |
Fabio Manganiello | 31ef9515f8 | |
Fabio Manganiello | d844890ab2 |
|
@ -19,3 +19,6 @@ platypush/requests
|
||||||
/http-client.env.json
|
/http-client.env.json
|
||||||
/platypush/backend/http/static/css/dist
|
/platypush/backend/http/static/css/dist
|
||||||
/tests/etc/dashboards
|
/tests/etc/dashboards
|
||||||
|
.coverage
|
||||||
|
coverage.xml
|
||||||
|
Session.vim
|
||||||
|
|
|
@ -56,11 +56,17 @@ upload-pip-package:
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
script:
|
script:
|
||||||
|
# Update the CI/CD configuration
|
||||||
|
- cd ~/platypush-ci-cd
|
||||||
|
- git pull
|
||||||
|
- cd -
|
||||||
|
# Build the package
|
||||||
- rm -rf build dist *.egg-info
|
- rm -rf build dist *.egg-info
|
||||||
- export VERSION=$(grep -e '^\s*__version__\s*=' platypush/__init__.py | sed -r -e 's/^\s*__version__\s*=\s*.(.+?).\s*$/\1/')
|
- export VERSION=$(grep -e '^\s*__version__\s*=' platypush/__init__.py | sed -r -e 's/^\s*__version__\s*=\s*.(.+?).\s*$/\1/')
|
||||||
- source ~/.credentials/pypi.env
|
- source ~/.credentials/pypi.env
|
||||||
- python setup.py sdist bdist_wheel
|
- python setup.py sdist bdist_wheel
|
||||||
# Upload to PyPI
|
# Upload to PyPI
|
||||||
- twine upload ./dist/platypush-${VERSION}.tar.gz
|
- twine upload --repository platypush ./dist/platypush-${VERSION}.tar.gz
|
||||||
# Upload to the local package repository
|
# Upload to the local package repository
|
||||||
- TWINE_PASSWORD=$LOCAL_TWINE_PASSWORD TWINE_USERNAME=$LOCAL_TWINE_USERNAME twine upload --repository-url https://git.platypush.tech/api/v4/projects/3/packages/pypi dist/platypush-${VERSION}.tar.gz
|
- TWINE_USERNAME=$LOCAL_TWINE_USERNAME TWINE_PASSWORD=$LOCAL_TWINE_PASSWORD twine upload --repository-url https://git.platypush.tech/api/v4/projects/3/packages/pypi dist/platypush-${VERSION}.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.1.0
|
||||||
|
hooks:
|
||||||
|
# - id: trailing-whitespace
|
||||||
|
# - id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-json
|
||||||
|
- id: check-xml
|
||||||
|
- id: check-symlinks
|
||||||
|
- id: check-added-large-files
|
||||||
|
|
||||||
|
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
|
||||||
|
rev: v1.1.2
|
||||||
|
hooks:
|
||||||
|
- id: markdown-toc
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: 4.0.1
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies:
|
||||||
|
- flake8-bugbear
|
||||||
|
- flake8-comprehensions
|
||||||
|
- flake8-simplify
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.3.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
176
CHANGELOG.md
176
CHANGELOG.md
|
@ -3,15 +3,167 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Removed `clipboard` backend. Enabling the `clipboard` plugin will also enable
|
||||||
|
clipboard monitoring, with no need for an additional backend.
|
||||||
|
|
||||||
|
## [0.23.3] - 2022-06-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `ntfy` integration (see #219).
|
||||||
|
- Support for a default `config.yaml` if one isn't specified in the default
|
||||||
|
locations.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The HTTP server dependencies are now marked as required, since the default
|
||||||
|
`config.yaml` will have the HTTP backend enabled by default in order to allow
|
||||||
|
the creation of a first user.
|
||||||
|
- Updated Vue.js frontend dependencies to the latest version.
|
||||||
|
- Removed bulma from the frontend dependencies, making the frontend much
|
||||||
|
lighter and loading times much faster.
|
||||||
|
- Other UI improvements.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- More reliable cronjobs in case of DST change or any clock changes in general
|
||||||
|
(see #217).
|
||||||
|
- Fixed `--redis-queue` argument.
|
||||||
|
|
||||||
|
## [0.23.2] - 2022-03-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for asynchronous events over GPIO PINs. It is now possible to specify
|
||||||
|
a list of `monitored_pins` in the [`gpio`
|
||||||
|
plugin](https://git.platypush.tech/platypush/platypush/-/blob/master/platypush/plugins/gpio/__init__.py)
|
||||||
|
configuration. A change in the value on those GPIO PINs (caused by e.g. a
|
||||||
|
button, a binary sensor or a probe) will trigger a
|
||||||
|
`platypush.message.event.gpio.GPIOEvent` that you can use in your automation
|
||||||
|
scripts.
|
||||||
|
|
||||||
|
- Simplified script API to interact with platform variables
|
||||||
|
(closes [#206](https://git.platypush.tech/platypush/platypush/-/issues/206)).
|
||||||
|
You can now read and write stored variables in your Platypush scripts through
|
||||||
|
a much more intuitive interface compared to explicitly using the `variable`
|
||||||
|
plugin explicitly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from platypush.context import Variable
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
my_var = Variable.get('my_var')
|
||||||
|
my_var = int(my_var) + 1
|
||||||
|
Variable.set(my_var=my_var)
|
||||||
|
```
|
||||||
|
|
||||||
|
## [0.23.0] - 2022-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added [Jellyfin integration](https://git.platypush.tech/platypush/platypush/-/issues/208).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Merged several PRs from `dependabot`.
|
||||||
|
|
||||||
|
- Fixed management of the `CN` field in the `calendar.ical` plugin.
|
||||||
|
|
||||||
|
## [0.22.10] - 2022-02-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Refactored the `dbus` integration. The plugin and backend have been merged into a
|
||||||
|
single plugin component, and the ability to subscribe to custom signals has been
|
||||||
|
added.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Proper support for empty payloads on the integrations that trigger a `SensorDataChangeEvent`.
|
||||||
|
- Fixed possible infinite recursion on the Pushbullet integration in case of errors where the
|
||||||
|
error and close handlers keep calling each other in a loop.
|
||||||
|
|
||||||
|
## [0.22.9] - 2022-01-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `rss` integration (replaces the cumbersome and deprecated `backend.http.poll`).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed timezone handling in calendar integrations.
|
||||||
|
- Fixed handling of ignored directories in the `file.monitor` backend.
|
||||||
|
|
||||||
|
## [0.22.8] - 2021-12-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for audio tracks in Plex integration.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Web server uWSGI wrapper changed from `uwsgi` to `gunicorn`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed client ID assignment logic in MQTT backends to prevent client ID clashes and reconnections
|
||||||
|
(closes #205).
|
||||||
|
- Updated LTR559 integration to be compatible with the new API.
|
||||||
|
- Updated Chromecast integration to be compatible with `pychromecast >= 10`.
|
||||||
|
- Better handling of media errors.
|
||||||
|
|
||||||
|
## [0.22.6] - 2021-11-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for converting webpages to markdown in `http.webpage.simplify`
|
||||||
|
even if no `outfile` is specified.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Improved robustness of the ICal calendar parser in case some fields (e.g. `*status`)
|
||||||
|
are not defined.
|
||||||
|
|
||||||
|
## [0.22.5] - 2021-11-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `mastodon` plugin.
|
||||||
|
- Added `chat.irc` plugin.
|
||||||
|
- Added `mailgun` plugin.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `switchbot.status` method in case of virtual devices.
|
||||||
|
- Fixed `platypush[alexa]` optional package installation.
|
||||||
|
|
||||||
|
## [0.22.4] - 2021-10-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for IR virtual devices in Switchbot plugin.
|
||||||
|
- Added [`google.maps.get_travel_time`](https://docs.platypush.tech/platypush/plugins/google.maps.html#platypush.plugins.google.maps.GoogleMapsPlugin.get_travel_time)
|
||||||
|
method (closes #115).
|
||||||
|
- Support for custom YouTube video/audio formats on media plugins.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Responses for requests received over an MQTT backend are now delivered to the right topic
|
||||||
|
(`<device_base_topic>/responses/<msg_id>`).
|
||||||
|
- Various fixes on media plugins.
|
||||||
|
|
||||||
## [0.22.3] - 2021-10-01
|
## [0.22.3] - 2021-10-01
|
||||||
|
|
||||||
## Added
|
### Added
|
||||||
|
|
||||||
- `gotify` integration (see #198).
|
- `gotify` integration (see #198).
|
||||||
|
|
||||||
## [0.22.2] - 2021-09-25
|
## [0.22.2] - 2021-09-25
|
||||||
|
|
||||||
## Added
|
### Added
|
||||||
|
|
||||||
- `ngrok` integration (see #196).
|
- `ngrok` integration (see #196).
|
||||||
|
|
||||||
|
@ -23,11 +175,11 @@ Given the high speed of development in the first phase, changes are being report
|
||||||
|
|
||||||
- Fixed bug on empty popcorn API responses.
|
- Fixed bug on empty popcorn API responses.
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
|
|
||||||
- Created CI Gitlab pipeline to replace the Platypush event-based pre-existing pipeline.
|
- Created CI Gitlab pipeline to replace the Platypush event-based pre-existing pipeline.
|
||||||
|
|
||||||
## Removed
|
### Removed
|
||||||
|
|
||||||
- Removed docs references to removed/abstract integrations.
|
- Removed docs references to removed/abstract integrations.
|
||||||
|
|
||||||
|
@ -44,7 +196,7 @@ Given the high speed of development in the first phase, changes are being report
|
||||||
operate instead of pydoc strings conventions or `config.yaml` conventions.
|
operate instead of pydoc strings conventions or `config.yaml` conventions.
|
||||||
|
|
||||||
- `platyvenv start` now starts the environment process synchronously and it prints
|
- `platyvenv start` now starts the environment process synchronously and it prints
|
||||||
stdout/stderr instead of redirecting it to the logs dir (previous behaviour:
|
stdout/stderr instead of redirecting it to the logs dir (previous behaviour:
|
||||||
`platyvenv start` used to start the process asynchronously and the logs were stored
|
`platyvenv start` used to start the process asynchronously and the logs were stored
|
||||||
to `~/.local/share/platypush/venv/<env>/logs/<stdout|stderr>.log`).
|
to `~/.local/share/platypush/venv/<env>/logs/<stdout|stderr>.log`).
|
||||||
|
|
||||||
|
@ -84,17 +236,17 @@ Given the high speed of development in the first phase, changes are being report
|
||||||
|
|
||||||
- Added `switchbot` plugin to interact with Switchbot devices over the cloud API instead of
|
- Added `switchbot` plugin to interact with Switchbot devices over the cloud API instead of
|
||||||
directly accessing the device's Bluetooth interface.
|
directly accessing the device's Bluetooth interface.
|
||||||
|
|
||||||
- Added `marshmallow` dependency - it will be used from now own to dump and document schemas
|
- 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
|
and responses instead of the currently mixed approach with `Response` objects and plain
|
||||||
dictionaries and lists.
|
dictionaries and lists.
|
||||||
|
|
||||||
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
|
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
|
||||||
|
|
||||||
- Added generic joystick backend `backend.joystick.jstest` which uses `jstest` from the
|
- 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
|
standard `joystick` system package to read the state of joysticks not compatible with
|
||||||
`python-inputs`.
|
`python-inputs`.
|
||||||
|
|
||||||
- Added PWM PCA9685 plugin.
|
- Added PWM PCA9685 plugin.
|
||||||
|
|
||||||
- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where
|
- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where
|
||||||
|
@ -151,7 +303,7 @@ Given the high speed of development in the first phase, changes are being report
|
||||||
|
|
||||||
- Added `<Camera>` dashboard widget.
|
- 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 custom dashboard widgets with customized (see https://git.platypush.tech/platypush/platypush/wiki/Backends#creating-custom-widgets).
|
||||||
|
|
||||||
- Added support for controls on `music.mpd` dashboard widget.
|
- Added support for controls on `music.mpd` dashboard widget.
|
||||||
|
|
||||||
|
@ -198,7 +350,7 @@ Given the high speed of development in the first phase, changes are being report
|
||||||
|
|
||||||
- Added support for a static list of devices to actively scan to the `bluetooth.scanner` backend
|
- 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)).
|
(see [#174](https://git.platypush.tech/platypush/platypush/-/issues/174)).
|
||||||
|
|
||||||
- Added `weather.openweathermap` plugin and backend, which replaces `weather.darksky`, since the
|
- Added `weather.openweathermap` plugin and backend, which replaces `weather.darksky`, since the
|
||||||
Darksky API will be completely shut down by the end of 2021.
|
Darksky API will be completely shut down by the end of 2021.
|
||||||
|
|
||||||
|
@ -206,14 +358,14 @@ Given the high speed of development in the first phase, changes are being report
|
||||||
|
|
||||||
- Cron expressions should adhere to the UNIX cronjob standard and use the machine local time,
|
- 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)).
|
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.
|
- Better management of Z-Wave values types from the UI.
|
||||||
|
|
||||||
- Disable logging for `ZwaveValueEvent` events - they tend to be very verbose and
|
- 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
|
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
|
websocket clients though, so you can still debug Z-Wave values issues from the browser
|
||||||
developer console (enable debug traces).
|
developer console (enable debug traces).
|
||||||
|
|
||||||
- Added suffix to the `zigbee.mqtt` backend default `client_id` to prevent clashes with
|
- Added suffix to the `zigbee.mqtt` backend default `client_id` to prevent clashes with
|
||||||
the default `mqtt` backend `client_id`.
|
the default `mqtt` backend `client_id`.
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,6 @@ Guidelines:
|
||||||
an `extras_require` entry.
|
an `extras_require` entry.
|
||||||
- In the plugin/backend class pydoc string.
|
- In the plugin/backend class pydoc string.
|
||||||
- In the `manifest.yaml` - refer to the Wiki (how to write
|
- In the `manifest.yaml` - refer to the Wiki (how to write
|
||||||
[plugins](https://git.platypush.tech/platypush/platypush/-/wikis/writing-your-own-plugins)
|
[plugins](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-plugins)
|
||||||
and [backends](https://git.platypush.tech/platypush/platypush/-/wikis/writing-your-own-backends))
|
and [backends](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-backends))
|
||||||
for examples on how to write an extension manifest file.
|
for examples on how to write an extension manifest file.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
recursive-include platypush/backend/http/webapp/dist *
|
recursive-include platypush/backend/http/webapp/dist *
|
||||||
include platypush/plugins/http/webpage/mercury-parser.js
|
include platypush/plugins/http/webpage/mercury-parser.js
|
||||||
|
include platypush/config/*.yaml
|
||||||
global-include manifest.yaml
|
global-include manifest.yaml
|
||||||
|
|
501
README.md
501
README.md
|
@ -6,39 +6,82 @@ Platypush
|
||||||
[![pip version](https://img.shields.io/pypi/v/platypush.svg?style=flat)](https://pypi.python.org/pypi/platypush/)
|
[![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)
|
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
|
||||||
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
|
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
|
||||||
|
[![Join chat on Matrix](https://img.shields.io/matrix/:platypush?server_fqdn=matrix.platypush.tech)](https://matrix.to/#/#platypush:matrix.platypush.tech)
|
||||||
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
|
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
|
||||||
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
|
[![Language grade: 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)
|
[![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)
|
||||||
|
|
||||||
|
<!-- toc -->
|
||||||
|
|
||||||
|
- [Architecture](#architecture)
|
||||||
|
* [Plugins](#plugins)
|
||||||
|
* [Actions](#actions)
|
||||||
|
* [Backends](#backends)
|
||||||
|
* [Events](#events)
|
||||||
|
* [Hooks](#hooks)
|
||||||
|
* [Procedures](#procedures)
|
||||||
|
* [Cronjobs](#cronjobs)
|
||||||
|
* [The web interface](#the-web-interface)
|
||||||
|
- [Installation](#installation)
|
||||||
|
* [System installation](#system-installation)
|
||||||
|
+ [Install through `pip`](#install-through-pip)
|
||||||
|
+ [Install through a system package manager](#install-through-a-system-package-manager)
|
||||||
|
+ [Install from sources](#install-from-sources)
|
||||||
|
* [Installing the dependencies for your extensions](#installing-the-dependencies-for-your-extensions)
|
||||||
|
+ [Install via `extras` name](#install-via-extras-name)
|
||||||
|
+ [Install via `manifest.yaml`](#install-via-manifestyaml)
|
||||||
|
+ [Check the instructions reported in the documentation](#check-the-instructions-reported-in-the-documentation)
|
||||||
|
* [Virtual environment installation](#virtual-environment-installation)
|
||||||
|
* [Docker installation](#docker-installation)
|
||||||
|
- [Mobile app](#mobile-app)
|
||||||
|
- [Tests](#tests)
|
||||||
|
- [Funding](#funding)
|
||||||
|
|
||||||
|
<!-- tocstop -->
|
||||||
|
|
||||||
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
|
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
|
||||||
|
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||
- The [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
|
- The [wiki](https://git.platypush.tech/platypush/platypush/wiki) also
|
||||||
|
contains many resources on getting started.
|
||||||
|
|
||||||
- Extensive documentation for all the available integrations and messages [is available](https://docs.platypush.tech/).
|
- Extensive documentation for all the available integrations and messages [is
|
||||||
|
available](https://docs.platypush.tech/).
|
||||||
|
|
||||||
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
|
- If you have issues/feature requests/enhancement ideas please [create an
|
||||||
|
issue](https://git.platypush.tech/platypush/platypush/-/issues).
|
||||||
|
|
||||||
- A [Reddit channel](https://www.reddit.com/r/platypush) is also available for more general questions.
|
- A [Reddit channel](https://www.reddit.com/r/platypush) is also available for
|
||||||
|
more general questions.
|
||||||
|
|
||||||
|
- A [Matrix instance](https://matrix.to/#/#platypush:matrix.platypush.tech) is
|
||||||
|
also available if you are looking for more interactive support.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Platypush is a general-purpose extensible platform for automation and integration across multiple services and devices.
|
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*)
|
It enables users to create their own self-hosted pieces of automation based on
|
||||||
and it provides a comprehensive and customizable user interface that collects everything you need to visualize and
|
events (*if this happens then do that*)
|
||||||
control under one roof.
|
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/),
|
It takes some concepts from [IFTTT](https://ifttt.com),
|
||||||
[Microsoft Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
|
[Tasker](https://tasker.joaoapps.com/), [Microsoft
|
||||||
[Home Assistant](https://www.home-assistant.io/) to provide an environment where the user can easily connect things
|
Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
|
||||||
together.
|
[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
|
Its ideal home is a single-board computer like a RaspberryPi that you can
|
||||||
and cloud automation in your own living room or garage, but it can easily run on any device that can run a Python
|
configure to orchestrate any home automation and cloud automation in your own
|
||||||
interpreter, and the bar for the hardware requirements is very low as well - I use it to run pieces of automation on
|
living room or garage, but it can easily run on any device that can run a
|
||||||
devices as powerful as a RaspberryPi Zero or an old Nokia N900 with Linux.
|
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:
|
You can use Platypush to do things like:
|
||||||
|
|
||||||
|
@ -47,8 +90,10 @@ You can use Platypush to do things like:
|
||||||
- [Create custom and privacy-secure voice assistants that run custom hooks on your phrases](https://blog.platypush.tech/article/Build-custom-voice-assistants)
|
- [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),
|
- 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),
|
[cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
||||||
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html) and
|
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html)
|
||||||
[machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html) to create smart
|
and [machine learning
|
||||||
|
models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html)
|
||||||
|
to create smart
|
||||||
pieces of automation for e.g.
|
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)
|
[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)
|
or [sound detection](https://blog.platypush.tech/article/Create-your-smart-baby-monitor-with-Platypush-and-Tensorflow)
|
||||||
|
@ -60,35 +105,53 @@ You can use Platypush to do things like:
|
||||||
- [Control your smart switches](https://docs.platypush.tech/en/latest/platypush/plugins/switch.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)
|
- [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)
|
- [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
|
- 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)
|
- [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)
|
- [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)
|
- 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://docs.platypush.tech/en/latest/plugins.html))
|
- ...and much more (basically, anything that comes with a [Platypush plugin](https://docs.platypush.tech/en/latest/plugins.html))
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The architecture of Platypush consists of a few simple pieces, orchestrated by a configuration file stored by default
|
The architecture of Platypush consists of a few simple pieces, orchestrated by
|
||||||
under [`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml):
|
a configuration file stored by default under
|
||||||
|
[`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml):
|
||||||
|
|
||||||
### [Plugins](https://docs.platypush.tech/en/latest/plugins.html)
|
### Plugins
|
||||||
|
|
||||||
They are integrations that do things - like
|
[Full list](https://docs.platypush.tech/en/latest/plugins.html)
|
||||||
[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.
|
|
||||||
|
|
||||||
The configuration of a plugin matches one-on-one that of its documented class constructor, so it's very straightforward
|
Plugins are integrations that do things - like [modify
|
||||||
to write a configuration for a plugin by reading its documentation:
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
light.hue:
|
light.hue:
|
||||||
|
@ -100,9 +163,11 @@ light.hue:
|
||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
Plugins expose *actions*, that match one-on-one the plugin class methods denoted by `@action`, so it's very
|
Plugins expose *actions*, that match one-on-one the plugin class methods
|
||||||
straightforward to invoke plugin actions by just reading the plugin documentation. They can be invoked directly from
|
denoted by `@action`, so it's very straightforward to invoke plugin actions by
|
||||||
your own scripts or they can be sent to the platform through any supported channel as simple JSON messages:
|
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
|
```json
|
||||||
{
|
{
|
||||||
|
@ -114,37 +179,54 @@ your own scripts or they can be sent to the platform through any supported chann
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Backends](https://docs.platypush.tech/en/latest/backends.html)
|
### Backends
|
||||||
|
|
||||||
They are background services that either listen for messages on channels (like an
|
[Full list](https://docs.platypush.tech/en/latest/backends.html)
|
||||||
[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
|
They are background services that either listen for messages on channels (like
|
||||||
to these services in JSON format. For example, in the case of the HTTP backend:
|
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
|
```shell
|
||||||
# Get a token
|
# Get a token
|
||||||
curl -XPOST -H 'Content-Type: application/json' -d '
|
curl -XPOST -H 'Content-Type: application/json' -d '
|
||||||
{
|
{
|
||||||
"username": "$YOUR_USER",
|
"username": "$YOUR_USER",
|
||||||
"password": "$YOUR_PASSWORD"
|
"password": "$YOUR_PASSWORD"
|
||||||
}' http://host:8008/auth
|
}' http://host:8008/auth
|
||||||
|
|
||||||
# Execute a request
|
# Execute a request
|
||||||
|
|
||||||
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
curl -XPOST -H 'Content-Type: application/json' \
|
||||||
|
-H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||||
{
|
{
|
||||||
"type": "request",
|
"type": "request",
|
||||||
"action": "tts.say",
|
"action": "tts.say",
|
||||||
|
@ -154,33 +236,38 @@ curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_
|
||||||
}' http://host:8008/execute
|
}' http://host:8008/execute
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Events](https://docs.platypush.tech/en/latest/events.html)
|
### Events
|
||||||
|
|
||||||
When a certain event occurs (e.g. a JSON request is received, or a
|
[Full list](https://docs.platypush.tech/en/latest/events.html)
|
||||||
[Bluetooth device is connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
|
|
||||||
or a
|
When a certain event occurs (e.g. a JSON request is received, or a [Bluetooth
|
||||||
[Flic button is pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
|
device is
|
||||||
or some
|
connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
|
||||||
[speech is detected on the voice assistant service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
|
or a [Flic button is
|
||||||
or an
|
pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
|
||||||
[RSS feed has new items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
|
or some [speech is detected on the voice assistant
|
||||||
or a
|
service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
|
||||||
[new email is received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
|
or an [RSS feed has new
|
||||||
or a
|
items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
|
||||||
[new track is played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
|
or a [new email is
|
||||||
or an
|
received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
|
||||||
[NFC tag is detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
|
or a [new track is
|
||||||
or
|
played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
|
||||||
[new sensor data is available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
|
or an [NFC tag is
|
||||||
or
|
detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
|
||||||
[a value of a Zigbee device changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
|
or [new sensor data is
|
||||||
etc.), the associated backend will trigger an [event](https://docs.platypush.tech/en/latest/events.html).
|
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
|
### Hooks
|
||||||
|
|
||||||
Event hooks are custom pieces of logic that will be run when a certain event is triggered. Hooks are the glue that
|
Event hooks are custom pieces of logic that will be run when a certain event is
|
||||||
connects events to actions, exposing a paradigm similar to IFTTT (_if a certain event happens then run these actions_).
|
triggered. Hooks are the glue that connects events to actions, exposing a
|
||||||
They can declared as:
|
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).
|
- Sections of the [`config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml).
|
||||||
Example:
|
Example:
|
||||||
|
@ -204,9 +291,10 @@ event.hook.SearchSongVoiceCommand:
|
||||||
resource: ${output[0]['file']}
|
resource: ${output[0]['file']}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Stand-alone Python scripts stored under `~/.config/platypush/scripts` and will be dynamically imported at start time.
|
- 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):
|
[Example](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/hook.py):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from platypush.event.hook import hook
|
from platypush.event.hook import hook
|
||||||
from platypush.utils import run
|
from platypush.utils import run
|
||||||
|
@ -225,13 +313,17 @@ def on_music_play_command(event, title=None, artist=None, **context):
|
||||||
|
|
||||||
### Procedures
|
### Procedures
|
||||||
|
|
||||||
Procedures are pieces of custom logic that can be executed as atomic actions using `procedure.<name>` as an action name.
|
Procedures are pieces of custom logic that can be executed as atomic actions
|
||||||
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` -
|
using `procedure.<name>` as an action name.
|
||||||
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
|
They can be defined either in the `config.yaml` or as Python scripts stored
|
||||||
that a certain thresholds, says a welcome home message using the TTS engine and starts playing the music:
|
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
|
```yaml
|
||||||
procedure.at_home:
|
procedure.at_home:
|
||||||
|
@ -254,7 +346,7 @@ procedure.at_home:
|
||||||
Python example:
|
Python example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Content of ~/.config/platypush/scripts/home.py
|
# Content of ~/.config/platypush/scripts/home.py
|
||||||
from platypush.procedure import procedure
|
from platypush.procedure import procedure
|
||||||
from platypush.utils import run
|
from platypush.utils import run
|
||||||
|
|
||||||
|
@ -268,11 +360,12 @@ def at_home(**context):
|
||||||
run('music.mpd.play')
|
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
|
In either case, you can easily trigger the at-home procedure by sending an
|
||||||
example, over the HTTP backend:
|
action request message to a backend - for example, over the HTTP backend:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
curl -XPOST -H 'Content-Type: application/json' \
|
||||||
|
-H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||||
{
|
{
|
||||||
"type": "request",
|
"type": "request",
|
||||||
"action": "procedure.at_home"
|
"action": "procedure.at_home"
|
||||||
|
@ -281,15 +374,18 @@ curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_
|
||||||
|
|
||||||
### Cronjobs
|
### Cronjobs
|
||||||
|
|
||||||
Cronjobs are pieces of logic that will be run at regular intervals, expressed in crontab-compatible syntax.
|
Cronjobs are pieces of logic that will be run at regular intervals, expressed
|
||||||
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` as
|
in crontab-compatible syntax. They can be defined either in the `config.yaml`
|
||||||
functions labelled by the `@cron` decorator.
|
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
|
Note that seconds are also supported (unlike the standard crontab definition),
|
||||||
standard crontab format, they are at the end of the cron expression, so the expression is actually in the format
|
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>`.
|
`<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 example for a cronjob that is executed every 30 seconds and checks if a
|
||||||
|
Bluetooth device is nearby:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cron.check_bt_device:
|
cron.check_bt_device:
|
||||||
|
@ -308,7 +404,7 @@ cron.check_bt_device:
|
||||||
Python example:
|
Python example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Content of ~/.config/platypush/scripts/bt_cron.py
|
# Content of ~/.config/platypush/scripts/bt_cron.py
|
||||||
from platypush.cron import cron
|
from platypush.cron import cron
|
||||||
from platypush.utils import run
|
from platypush.utils import run
|
||||||
|
|
||||||
|
@ -323,15 +419,18 @@ def check_bt_device(**context):
|
||||||
|
|
||||||
### The web interface
|
### The web interface
|
||||||
|
|
||||||
If [`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html) is enabled then a web interface
|
If
|
||||||
will be provided by default on `http://host:8008/`. Besides using the `/execute` endpoint for running requests, the
|
[`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html)
|
||||||
built-in web server also provides a full-featured interface that groups together the controls for most of the plugins -
|
is enabled then a web interface will be provided by default on
|
||||||
e.g. sensors, switches, music controls and search, media library and torrent management, lights, Zigbee/Z-Wave devices
|
`http://host:8008/`. Besides using the `/execute` endpoint for running
|
||||||
and so on. The UI is responsive and mobile-friendly.
|
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
|
The web service also provides means for the user to create [custom
|
||||||
[custom dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml) that can
|
dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml)
|
||||||
be used to show information from multiple sources on a large screen.
|
that can be used to show information from multiple sources on a large screen.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -340,36 +439,69 @@ be used to show information from multiple sources on a large screen.
|
||||||
Platypush uses Redis to deliver and store requests and temporary messages:
|
Platypush uses Redis to deliver and store requests and temporary messages:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Example for Debian-based distributions
|
# Example for Debian-based distributions
|
||||||
[sudo] apt-get install redis-server
|
[sudo] apt-get install redis-server
|
||||||
|
|
||||||
# Enable and start the service
|
# Enable and start the service
|
||||||
[sudo] systemctl enable redis
|
[sudo] systemctl enable redis
|
||||||
[sudo] systemctl start redis
|
[sudo] systemctl start redis
|
||||||
```
|
```
|
||||||
|
|
||||||
To install the core platform:
|
#### Install through `pip`
|
||||||
|
|
||||||
* The `pip` way:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
[sudo] pip3 install platypush
|
[sudo] pip3 install platypush
|
||||||
```
|
```
|
||||||
|
|
||||||
* The sources way:
|
#### Install through a system package manager
|
||||||
|
|
||||||
|
Note: currently only Arch Linux and derived distributions are supported.
|
||||||
|
|
||||||
|
You can either install the
|
||||||
|
[`platypush`](https://aur.archlinux.org/packages/platypush) package (for the
|
||||||
|
latest stable version) or the
|
||||||
|
[`platypush-git`](https://aur.archlinux.org/packages/platypush-git) package
|
||||||
|
(for the latest git version) through your favourite AUR package manager. For
|
||||||
|
example, using `yay`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yay platypush
|
||||||
|
# Or
|
||||||
|
yay platypush-git
|
||||||
|
```
|
||||||
|
|
||||||
|
The Arch Linux packages on AUR are automatically updated upon new git commits
|
||||||
|
or tags.
|
||||||
|
|
||||||
|
#### Install from sources
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://git.platypush.tech/platypush/platypush.git
|
git clone https://git.platypush.tech/platypush/platypush.git
|
||||||
cd platypush
|
cd platypush
|
||||||
|
[sudo] pip install .
|
||||||
|
# Or
|
||||||
[sudo] python3 setup.py install
|
[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
|
### Installing the dependencies for your extensions
|
||||||
extension:
|
|
||||||
|
|
||||||
#### Check their `extras` name in [`extras_require` under `setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72).
|
After installing the base platform, you may want to check the dependencies and
|
||||||
|
configuration required by the extensions that you wish to use. There are a few
|
||||||
|
ways to check the dependencies required by an extension:
|
||||||
|
|
||||||
If you follow this route then you can install the extra dependencies in one of the following ways:
|
#### Install via `extras` name
|
||||||
|
|
||||||
|
All the extensions that require extra dependencies are listed in the
|
||||||
|
[`extras_require` section under
|
||||||
|
`setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72).
|
||||||
|
|
||||||
|
#### Install via `manifest.yaml`
|
||||||
|
|
||||||
|
All the plugins and backends have a `manifest.yaml` file in their source folder.
|
||||||
|
Any extra dependencies are listed there
|
||||||
|
|
||||||
|
If you followed the `extras` or `manifest.yaml` way to discover the
|
||||||
|
dependencies, then you can install them in two ways:
|
||||||
|
|
||||||
1. `pip` installation:
|
1. `pip` installation:
|
||||||
|
|
||||||
|
@ -383,15 +515,18 @@ If you follow this route then you can install the extra dependencies in one of t
|
||||||
cd $DIR_TO_PLATYPUSH
|
cd $DIR_TO_PLATYPUSH
|
||||||
[sudo] pip3 install '.[extra1,extra2,extra3]'
|
[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
|
#### Check the instructions reported in the documentation
|
||||||
installed.
|
|
||||||
|
|
||||||
After installing the dependencies, create a configuration file under `~/.config/platypush/config.yaml` (the application
|
If you follow this route then simply run the commands listed in the
|
||||||
can load the configuration from another location through the `-c` option) containing the configuration of the backend
|
[plugin/backend documentation](https://docs.platypush.tech) to get the
|
||||||
and plugins that you want to use, and add any hooks and procedures for your use case.
|
dependencies installed.
|
||||||
|
|
||||||
|
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:
|
You can then start the service by simply running:
|
||||||
|
|
||||||
|
@ -400,89 +535,100 @@ platypush
|
||||||
```
|
```
|
||||||
|
|
||||||
It's advised to run it as a systemd service though - simply copy the provided
|
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
|
[`.service`
|
||||||
`~/.config/systemd/user`, check if the path of `platypush` matches the path where it's installed on your system, and
|
file](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/systemd/platypush.service)
|
||||||
start the service via `systemctl`:
|
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
|
```shell
|
||||||
systemctl --user start platypush
|
systemctl --user start platypush
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Virtual environment installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-virtual-environment)
|
### Virtual environment installation
|
||||||
|
|
||||||
Platypush provides a script named `platyvenv` that can parse a `config.yaml` and automatically create a virtual
|
Platypush provides a script named `platyvenv` that can parse a `config.yaml`
|
||||||
environment (under `~/.local/share/platypush/venv/<device_id>`) with all the dependencies required by the configured
|
and automatically create a virtual environment (under
|
||||||
integrations.
|
`~/.local/share/platypush/venv/<device_id>`) with all the dependencies required
|
||||||
|
by the configured integrations.
|
||||||
|
|
||||||
1. Create the environment from a configuration file:
|
1. Create the environment from a configuration file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
platyvenv build -c /path/to/config.yaml
|
platyvenv build -c /path/to/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the service from the virtual environment:
|
2. Start the service from the virtual environment:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# device_id matches either the hostname or the device_id in config.yaml
|
# device_id matches either the hostname or the device_id in config.yaml
|
||||||
platyvenv start device_id
|
platyvenv start device_id
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Stop the instance:
|
3. Stop the instance:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
platyvenv stop device_id
|
platyvenv stop device_id
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Remove the instance:
|
4. Remove the instance:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
platyvenv rm device_id
|
platyvenv rm device_id
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Docker installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-container)
|
[Wiki instructions](https://git.platypush.tech/platypush/platypush/wiki/Run-platypush-in-a-virtual-environment)
|
||||||
|
|
||||||
You can also install Platypush in a container - the application provides a script named `platydock` that automatically
|
### Docker installation
|
||||||
creates a container instance from a `config.yaml`:
|
|
||||||
|
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:
|
1. Create the container from a configuration file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
platydock build -c /path/to/config.yaml
|
platydock build -c /path/to/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Start the container:
|
2. Start the container:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# device_id matches either the hostname or the device_id in config.yaml
|
# device_id matches either the hostname or the device_id in config.yaml
|
||||||
platydock start device_id
|
platydock start device_id
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Stop the instance:
|
3. Stop the instance:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
platydock stop device_id
|
platydock stop device_id
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Remove the instance:
|
4. Remove the instance:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
platydock rm device_id
|
platydock rm device_id
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that both the virtual environment and Docker container option offer the possibility to include extra YAML configuration
|
Note that both the virtual environment and Docker container option offer the
|
||||||
files in the main `config.yaml` through the `include` directive (as long as they are in the same folder as the main
|
possibility to include extra YAML configuration files in the main `config.yaml`
|
||||||
`config.yaml`), as well as external Python scripts in a `scripts` directory in the same folder as the `config.yaml`.
|
through the `include` directive (as long as they are in the same folder as the
|
||||||
|
main `config.yaml`), as well as external Python scripts in a `scripts`
|
||||||
|
directory in the same folder as the `config.yaml`.
|
||||||
|
|
||||||
|
[Wiki instructions](https://git.platypush.tech/platypush/platypush/wiki/Run-platypush-in-a-container)
|
||||||
|
|
||||||
## Mobile app
|
## Mobile app
|
||||||
|
|
||||||
An [official Android app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on the F-Droid store.
|
An [official Android
|
||||||
It allows to easily discover and manage multiple Platypush services on a network through the web interface, and it
|
app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on
|
||||||
easily brings the power of Platypush to your fingertips.
|
the F-Droid store. It allows to easily discover and manage multiple Platypush
|
||||||
|
services on a network through the web interface, and it easily brings the power
|
||||||
|
of Platypush to your fingertips.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
|
To run the tests simply run `pytest` either from the project root folder or the
|
||||||
Or run the following command from the project root folder:
|
`tests/` folder. Or run the following command from the project root folder:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python -m tests
|
python -m tests
|
||||||
|
@ -494,10 +640,11 @@ python -m tests
|
||||||
|
|
||||||
If you use and love Platypush, please consider [buying me a coffee/beer](https://paypal.me/fabiomanganiello).
|
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
|
I've been working on Platypush all by myself in my spare time for the past few
|
||||||
open and free.
|
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
|
If you like this product, please consider supporting - I'm definitely not
|
||||||
I'd love to have at least the costs for the server covered by users.
|
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.
|
Issues and requests opened by donors will also be given priority over others.
|
||||||
|
|
|
@ -3,10 +3,12 @@ import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from random import randint
|
||||||
from typing import Union, List
|
from typing import Union, List
|
||||||
|
|
||||||
from docutils import nodes
|
from docutils import nodes
|
||||||
from docutils.parsers.rst import Directive
|
from docutils.parsers.rst import Directive
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
|
||||||
class SchemaDirective(Directive):
|
class SchemaDirective(Directive):
|
||||||
|
@ -22,10 +24,36 @@ class SchemaDirective(Directive):
|
||||||
|
|
||||||
sys.path.insert(0, _schemas_path)
|
sys.path.insert(0, _schemas_path)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _get_field_value(field) -> str:
|
def _get_field_value(cls, field):
|
||||||
metadata = getattr(field, 'metadata', {})
|
metadata = getattr(field, 'metadata', {})
|
||||||
return metadata.get('example', metadata.get('description', str(field.__class__.__name__).lower()))
|
if metadata.get('example'):
|
||||||
|
return metadata['example']
|
||||||
|
if metadata.get('description'):
|
||||||
|
return metadata['description']
|
||||||
|
|
||||||
|
if isinstance(field, fields.Number):
|
||||||
|
return randint(1, 99)
|
||||||
|
if isinstance(field, fields.Boolean):
|
||||||
|
return bool(randint(0, 1))
|
||||||
|
if isinstance(field, fields.URL):
|
||||||
|
return 'https://example.org'
|
||||||
|
if isinstance(field, fields.List):
|
||||||
|
return [cls._get_field_value(field.inner)]
|
||||||
|
if isinstance(field, fields.Dict):
|
||||||
|
return {
|
||||||
|
cls._get_field_value(field.key_field) if field.key_field else 'key':
|
||||||
|
cls._get_field_value(field.value_field) if field.value_field else 'value'
|
||||||
|
}
|
||||||
|
if isinstance(field, fields.Nested):
|
||||||
|
ret = {
|
||||||
|
name: cls._get_field_value(f)
|
||||||
|
for name, f in field.nested().fields.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ret] if field.many else ret
|
||||||
|
|
||||||
|
return str(field.__class__.__name__).lower()
|
||||||
|
|
||||||
def _parse_schema(self) -> Union[dict, List[dict]]:
|
def _parse_schema(self) -> Union[dict, List[dict]]:
|
||||||
m = self._schema_regex.match('\n'.join(self.content))
|
m = self._schema_regex.match('\n'.join(self.content))
|
||||||
|
|
|
@ -17,9 +17,7 @@ Backends
|
||||||
platypush/backend/button.flic.rst
|
platypush/backend/button.flic.rst
|
||||||
platypush/backend/camera.pi.rst
|
platypush/backend/camera.pi.rst
|
||||||
platypush/backend/chat.telegram.rst
|
platypush/backend/chat.telegram.rst
|
||||||
platypush/backend/clipboard.rst
|
|
||||||
platypush/backend/covid19.rst
|
platypush/backend/covid19.rst
|
||||||
platypush/backend/dbus.rst
|
|
||||||
platypush/backend/file.monitor.rst
|
platypush/backend/file.monitor.rst
|
||||||
platypush/backend/foursquare.rst
|
platypush/backend/foursquare.rst
|
||||||
platypush/backend/github.rst
|
platypush/backend/github.rst
|
||||||
|
|
|
@ -195,6 +195,10 @@ intersphinx_mapping = {'https://docs.python.org/': None}
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
todo_include_todos = True
|
todo_include_todos = True
|
||||||
|
|
||||||
|
autodoc_default_options = {
|
||||||
|
'inherited-members': True,
|
||||||
|
}
|
||||||
|
|
||||||
autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'google.assistant.embedded',
|
'google.assistant.embedded',
|
||||||
'google.assistant.library',
|
'google.assistant.library',
|
||||||
|
@ -212,7 +216,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'gevent.wsgi',
|
'gevent.wsgi',
|
||||||
'Adafruit_IO',
|
'Adafruit_IO',
|
||||||
'pyperclip',
|
'pyperclip',
|
||||||
'dbus',
|
'pydbus',
|
||||||
'inputs',
|
'inputs',
|
||||||
'inotify',
|
'inotify',
|
||||||
'omxplayer',
|
'omxplayer',
|
||||||
|
@ -272,7 +276,6 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'gi',
|
'gi',
|
||||||
'gi.repository',
|
'gi.repository',
|
||||||
'twilio',
|
'twilio',
|
||||||
'pytz',
|
|
||||||
'Adafruit_Python_DHT',
|
'Adafruit_Python_DHT',
|
||||||
'RPi.GPIO',
|
'RPi.GPIO',
|
||||||
'RPLCD',
|
'RPLCD',
|
||||||
|
@ -281,6 +284,13 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'aiohttp',
|
'aiohttp',
|
||||||
'watchdog',
|
'watchdog',
|
||||||
'pyngrok',
|
'pyngrok',
|
||||||
|
'irc',
|
||||||
|
'irc.bot',
|
||||||
|
'irc.strings',
|
||||||
|
'irc.client',
|
||||||
|
'irc.connection',
|
||||||
|
'irc.events',
|
||||||
|
'defusedxml',
|
||||||
]
|
]
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
|
@ -18,6 +18,7 @@ Events
|
||||||
platypush/events/clipboard.rst
|
platypush/events/clipboard.rst
|
||||||
platypush/events/covid19.rst
|
platypush/events/covid19.rst
|
||||||
platypush/events/custom.rst
|
platypush/events/custom.rst
|
||||||
|
platypush/events/dbus.rst
|
||||||
platypush/events/distance.rst
|
platypush/events/distance.rst
|
||||||
platypush/events/file.rst
|
platypush/events/file.rst
|
||||||
platypush/events/foursquare.rst
|
platypush/events/foursquare.rst
|
||||||
|
@ -26,11 +27,14 @@ Events
|
||||||
platypush/events/google.rst
|
platypush/events/google.rst
|
||||||
platypush/events/google.fit.rst
|
platypush/events/google.fit.rst
|
||||||
platypush/events/google.pubsub.rst
|
platypush/events/google.pubsub.rst
|
||||||
|
platypush/events/gotify.rst
|
||||||
|
platypush/events/gpio.rst
|
||||||
platypush/events/gps.rst
|
platypush/events/gps.rst
|
||||||
platypush/events/http.rst
|
platypush/events/http.rst
|
||||||
platypush/events/http.hook.rst
|
platypush/events/http.hook.rst
|
||||||
platypush/events/http.rss.rst
|
platypush/events/http.rss.rst
|
||||||
platypush/events/inotify.rst
|
platypush/events/inotify.rst
|
||||||
|
platypush/events/irc.rst
|
||||||
platypush/events/joystick.rst
|
platypush/events/joystick.rst
|
||||||
platypush/events/kafka.rst
|
platypush/events/kafka.rst
|
||||||
platypush/events/light.rst
|
platypush/events/light.rst
|
||||||
|
@ -45,9 +49,11 @@ Events
|
||||||
platypush/events/nextcloud.rst
|
platypush/events/nextcloud.rst
|
||||||
platypush/events/nfc.rst
|
platypush/events/nfc.rst
|
||||||
platypush/events/ngrok.rst
|
platypush/events/ngrok.rst
|
||||||
|
platypush/events/ntfy.rst
|
||||||
platypush/events/ping.rst
|
platypush/events/ping.rst
|
||||||
platypush/events/pushbullet.rst
|
platypush/events/pushbullet.rst
|
||||||
platypush/events/qrcode.rst
|
platypush/events/qrcode.rst
|
||||||
|
platypush/events/rss.rst
|
||||||
platypush/events/scard.rst
|
platypush/events/scard.rst
|
||||||
platypush/events/sensor.rst
|
platypush/events/sensor.rst
|
||||||
platypush/events/sensor.ir.rst
|
platypush/events/sensor.ir.rst
|
||||||
|
|
|
@ -6,13 +6,13 @@ Welcome to the Platypush reference of available plugins, backends and event type
|
||||||
For more information on Platypush check out:
|
For more information on Platypush check out:
|
||||||
|
|
||||||
* The `main page`_ of the project
|
* The `main page`_ of the project
|
||||||
* The `Gitlab page`_ of the project
|
* The `Gitea page`_ of the project
|
||||||
* The `online wiki`_ for quickstart and examples
|
* The `online wiki`_ for quickstart and examples
|
||||||
* The `Blog articles`_ for inspiration on use-cases possible projects
|
* The `Blog articles`_ for inspiration on use-cases possible projects
|
||||||
|
|
||||||
.. _main page: https://platypush.tech
|
.. _main page: https://platypush.tech
|
||||||
.. _Gitlab page: https://git.platypush.tech/platypush/platypush
|
.. _Gitea page: https://git.platypush.tech/platypush/platypush
|
||||||
.. _online wiki: https://git.platypush.tech/platypush/platypush/-/wikis/home
|
.. _online wiki: https://git.platypush.tech/platypush/platypush/wiki
|
||||||
.. _Blog articles: https://blog.platypush.tech
|
.. _Blog articles: https://blog.platypush.tech
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``clipboard``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.clipboard
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``dbus``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.dbus
|
|
||||||
:members:
|
|
|
@ -1,5 +1,5 @@
|
||||||
``platypush.message.event.chat.slack``
|
``chat.slack``
|
||||||
======================================
|
==============
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.chat.slack
|
.. automodule:: platypush.message.event.chat.slack
|
||||||
:members:
|
:members:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
``dbus``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.dbus
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``gotify``
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.gotify
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``gpio``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.gpio
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``irc``
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.irc
|
||||||
|
:members:
|
|
@ -1,5 +1,5 @@
|
||||||
``platypush.message.event.ngrok``
|
``ngrok``
|
||||||
=================================
|
=========
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.ngrok
|
.. automodule:: platypush.message.event.ngrok
|
||||||
:members:
|
:members:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
``ntfy``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.ntfy
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``rss``
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.rss
|
||||||
|
:members:
|
|
@ -1,5 +1,5 @@
|
||||||
``platypush.message.event.sun``
|
``sun``
|
||||||
===============================
|
=======
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.sun
|
.. automodule:: platypush.message.event.sun
|
||||||
:members:
|
:members:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
``chat.irc``
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.chat.irc
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``gotify``
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.gotify
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``mailgun``
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.mailgun
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``mastodon``
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.mastodon
|
||||||
|
:members:
|
|
@ -0,0 +1,6 @@
|
||||||
|
``media.jellyfin``
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.media.jellyfin
|
||||||
|
:members:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
``ntfy``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.ntfy
|
||||||
|
:members:
|
|
@ -0,0 +1,5 @@
|
||||||
|
``rss``
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.rss
|
||||||
|
:members:
|
|
@ -23,6 +23,7 @@ Plugins
|
||||||
platypush/plugins/camera.gstreamer.rst
|
platypush/plugins/camera.gstreamer.rst
|
||||||
platypush/plugins/camera.ir.mlx90640.rst
|
platypush/plugins/camera.ir.mlx90640.rst
|
||||||
platypush/plugins/camera.pi.rst
|
platypush/plugins/camera.pi.rst
|
||||||
|
platypush/plugins/chat.irc.rst
|
||||||
platypush/plugins/chat.telegram.rst
|
platypush/plugins/chat.telegram.rst
|
||||||
platypush/plugins/clipboard.rst
|
platypush/plugins/clipboard.rst
|
||||||
platypush/plugins/config.rst
|
platypush/plugins/config.rst
|
||||||
|
@ -43,6 +44,7 @@ Plugins
|
||||||
platypush/plugins/google.pubsub.rst
|
platypush/plugins/google.pubsub.rst
|
||||||
platypush/plugins/google.translate.rst
|
platypush/plugins/google.translate.rst
|
||||||
platypush/plugins/google.youtube.rst
|
platypush/plugins/google.youtube.rst
|
||||||
|
platypush/plugins/gotify.rst
|
||||||
platypush/plugins/gpio.rst
|
platypush/plugins/gpio.rst
|
||||||
platypush/plugins/gpio.sensor.accelerometer.rst
|
platypush/plugins/gpio.sensor.accelerometer.rst
|
||||||
platypush/plugins/gpio.sensor.bme280.rst
|
platypush/plugins/gpio.sensor.bme280.rst
|
||||||
|
@ -71,8 +73,11 @@ Plugins
|
||||||
platypush/plugins/luma.oled.rst
|
platypush/plugins/luma.oled.rst
|
||||||
platypush/plugins/mail.imap.rst
|
platypush/plugins/mail.imap.rst
|
||||||
platypush/plugins/mail.smtp.rst
|
platypush/plugins/mail.smtp.rst
|
||||||
|
platypush/plugins/mailgun.rst
|
||||||
|
platypush/plugins/mastodon.rst
|
||||||
platypush/plugins/media.chromecast.rst
|
platypush/plugins/media.chromecast.rst
|
||||||
platypush/plugins/media.gstreamer.rst
|
platypush/plugins/media.gstreamer.rst
|
||||||
|
platypush/plugins/media.jellyfin.rst
|
||||||
platypush/plugins/media.kodi.rst
|
platypush/plugins/media.kodi.rst
|
||||||
platypush/plugins/media.mplayer.rst
|
platypush/plugins/media.mplayer.rst
|
||||||
platypush/plugins/media.mpv.rst
|
platypush/plugins/media.mpv.rst
|
||||||
|
@ -91,6 +96,7 @@ Plugins
|
||||||
platypush/plugins/nextcloud.rst
|
platypush/plugins/nextcloud.rst
|
||||||
platypush/plugins/ngrok.rst
|
platypush/plugins/ngrok.rst
|
||||||
platypush/plugins/nmap.rst
|
platypush/plugins/nmap.rst
|
||||||
|
platypush/plugins/ntfy.rst
|
||||||
platypush/plugins/otp.rst
|
platypush/plugins/otp.rst
|
||||||
platypush/plugins/pihole.rst
|
platypush/plugins/pihole.rst
|
||||||
platypush/plugins/ping.rst
|
platypush/plugins/ping.rst
|
||||||
|
@ -99,6 +105,7 @@ Plugins
|
||||||
platypush/plugins/pwm.pca9685.rst
|
platypush/plugins/pwm.pca9685.rst
|
||||||
platypush/plugins/qrcode.rst
|
platypush/plugins/qrcode.rst
|
||||||
platypush/plugins/redis.rst
|
platypush/plugins/redis.rst
|
||||||
|
platypush/plugins/rss.rst
|
||||||
platypush/plugins/rtorrent.rst
|
platypush/plugins/rtorrent.rst
|
||||||
platypush/plugins/serial.rst
|
platypush/plugins/serial.rst
|
||||||
platypush/plugins/shell.rst
|
platypush/plugins/shell.rst
|
||||||
|
|
|
@ -6,11 +6,17 @@ from platypush.plugins import Plugin
|
||||||
from platypush.utils.manifest import get_manifests
|
from platypush.utils.manifest import get_manifests
|
||||||
|
|
||||||
|
|
||||||
|
def _get_inspect_plugin():
|
||||||
|
p = get_plugin('inspect')
|
||||||
|
assert p, 'Could not load the `inspect` plugin'
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
def get_all_plugins():
|
def get_all_plugins():
|
||||||
manifests = {mf.component_name for mf in get_manifests(Plugin)}
|
manifests = {mf.component_name for mf in get_manifests(Plugin)}
|
||||||
return {
|
return {
|
||||||
plugin_name: plugin_info
|
plugin_name: plugin_info
|
||||||
for plugin_name, plugin_info in get_plugin('inspect').get_all_plugins().output.items()
|
for plugin_name, plugin_info in _get_inspect_plugin().get_all_plugins().output.items()
|
||||||
if plugin_name in manifests
|
if plugin_name in manifests
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,17 +25,17 @@ def get_all_backends():
|
||||||
manifests = {mf.component_name for mf in get_manifests(Backend)}
|
manifests = {mf.component_name for mf in get_manifests(Backend)}
|
||||||
return {
|
return {
|
||||||
backend_name: backend_info
|
backend_name: backend_info
|
||||||
for backend_name, backend_info in get_plugin('inspect').get_all_backends().output.items()
|
for backend_name, backend_info in _get_inspect_plugin().get_all_backends().output.items()
|
||||||
if backend_name in manifests
|
if backend_name in manifests
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_all_events():
|
def get_all_events():
|
||||||
return get_plugin('inspect').get_all_events().output
|
return _get_inspect_plugin().get_all_events().output
|
||||||
|
|
||||||
|
|
||||||
def get_all_responses():
|
def get_all_responses():
|
||||||
return get_plugin('inspect').get_all_responses().output
|
return _get_inspect_plugin().get_all_responses().output
|
||||||
|
|
||||||
|
|
||||||
# noinspection DuplicatedCode
|
# noinspection DuplicatedCode
|
||||||
|
@ -100,16 +106,17 @@ Backends
|
||||||
|
|
||||||
# noinspection DuplicatedCode
|
# noinspection DuplicatedCode
|
||||||
def generate_events_doc():
|
def generate_events_doc():
|
||||||
|
from platypush.message import event as event_module
|
||||||
events_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'events.rst')
|
events_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'events.rst')
|
||||||
events_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'events')
|
events_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'events')
|
||||||
all_events = sorted(event for event in get_all_events().keys())
|
all_events = sorted(event for event in get_all_events().keys() if event)
|
||||||
|
|
||||||
for event in all_events:
|
for event in all_events:
|
||||||
event_file = os.path.join(events_dir, event[len('platypush.message.event.'):] + '.rst')
|
event_file = os.path.join(events_dir, event + '.rst')
|
||||||
if not os.path.exists(event_file):
|
if not os.path.exists(event_file):
|
||||||
header = '``{}``'.format(event)
|
header = '``{}``'.format(event)
|
||||||
divider = '=' * len(header)
|
divider = '=' * len(header)
|
||||||
body = '\n.. automodule:: {}\n :members:\n'.format(event)
|
body = '\n.. automodule:: {}.{}\n :members:\n'.format(event_module.__name__, event)
|
||||||
out = '\n'.join([header, divider, body])
|
out = '\n'.join([header, divider, body])
|
||||||
|
|
||||||
with open(event_file, 'w') as f:
|
with open(event_file, 'w') as f:
|
||||||
|
@ -127,21 +134,22 @@ Events
|
||||||
''')
|
''')
|
||||||
|
|
||||||
for event in all_events:
|
for event in all_events:
|
||||||
f.write(' platypush/events/' + event[len('platypush.message.event.'):] + '.rst\n')
|
f.write(' platypush/events/' + event + '.rst\n')
|
||||||
|
|
||||||
|
|
||||||
# noinspection DuplicatedCode
|
# noinspection DuplicatedCode
|
||||||
def generate_responses_doc():
|
def generate_responses_doc():
|
||||||
|
from platypush.message import response as response_module
|
||||||
responses_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'responses.rst')
|
responses_index = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'responses.rst')
|
||||||
responses_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'responses')
|
responses_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docs', 'source', 'platypush', 'responses')
|
||||||
all_responses = sorted(response for response in get_all_responses().keys())
|
all_responses = sorted(response for response in get_all_responses().keys() if response)
|
||||||
|
|
||||||
for response in all_responses:
|
for response in all_responses:
|
||||||
response_file = os.path.join(responses_dir, response[len('platypush.message.response.'):] + '.rst')
|
response_file = os.path.join(responses_dir, response + '.rst')
|
||||||
if not os.path.exists(response_file):
|
if not os.path.exists(response_file):
|
||||||
header = '``{}``'.format(response)
|
header = '``{}``'.format(response)
|
||||||
divider = '=' * len(header)
|
divider = '=' * len(header)
|
||||||
body = '\n.. automodule:: {}\n :members:\n'.format(response)
|
body = '\n.. automodule:: {}.{}\n :members:\n'.format(response_module.__name__, response)
|
||||||
out = '\n'.join([header, divider, body])
|
out = '\n'.join([header, divider, body])
|
||||||
|
|
||||||
with open(response_file, 'w') as f:
|
with open(response_file, 'w') as f:
|
||||||
|
@ -159,7 +167,7 @@ Responses
|
||||||
''')
|
''')
|
||||||
|
|
||||||
for response in all_responses:
|
for response in all_responses:
|
||||||
f.write(' platypush/responses/' + response[len('platypush.message.response.'):] + '.rst\n')
|
f.write(' platypush/responses/' + response + '.rst\n')
|
||||||
|
|
||||||
|
|
||||||
generate_plugins_doc()
|
generate_plugins_doc()
|
||||||
|
|
|
@ -23,13 +23,13 @@ from .message.response import Response
|
||||||
from .utils import set_thread_name, get_enabled_plugins
|
from .utils import set_thread_name, get_enabled_plugins
|
||||||
|
|
||||||
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
||||||
__version__ = '0.22.2'
|
__version__ = '0.23.3'
|
||||||
|
|
||||||
logger = logging.getLogger('platypush')
|
logger = logging.getLogger('platypush')
|
||||||
|
|
||||||
|
|
||||||
class Daemon:
|
class Daemon:
|
||||||
""" Main class for the Platypush daemon """
|
"""Main class for the Platypush daemon"""
|
||||||
|
|
||||||
# Configuration file (default: either ~/.config/platypush/config.yaml or
|
# Configuration file (default: either ~/.config/platypush/config.yaml or
|
||||||
# /etc/platypush/config.yaml
|
# /etc/platypush/config.yaml
|
||||||
|
@ -51,8 +51,15 @@ class Daemon:
|
||||||
# number of executions retries before a request fails
|
# number of executions retries before a request fails
|
||||||
n_tries = 2
|
n_tries = 2
|
||||||
|
|
||||||
def __init__(self, config_file=None, pidfile=None, requests_to_process=None,
|
def __init__(
|
||||||
no_capture_stdout=False, no_capture_stderr=False, redis_queue=None):
|
self,
|
||||||
|
config_file=None,
|
||||||
|
pidfile=None,
|
||||||
|
requests_to_process=None,
|
||||||
|
no_capture_stdout=False,
|
||||||
|
no_capture_stderr=False,
|
||||||
|
redis_queue=None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Constructor
|
Constructor
|
||||||
Params:
|
Params:
|
||||||
|
@ -80,8 +87,11 @@ class Daemon:
|
||||||
logging.basicConfig(**Config.get('logging'))
|
logging.basicConfig(**Config.get('logging'))
|
||||||
|
|
||||||
redis_conf = Config.get('backend.redis') or {}
|
redis_conf = Config.get('backend.redis') or {}
|
||||||
self.bus = RedisBus(redis_queue=self.redis_queue, on_message=self.on_message(),
|
self.bus = RedisBus(
|
||||||
**redis_conf.get('redis_args', {}))
|
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_stdout = no_capture_stdout
|
||||||
self.no_capture_stderr = no_capture_stderr
|
self.no_capture_stderr = no_capture_stderr
|
||||||
|
@ -98,33 +108,59 @@ class Daemon:
|
||||||
args -- Your sys.argv[1:] [List of strings]
|
args -- Your sys.argv[1:] [List of strings]
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--config', '-c', dest='config', required=False,
|
parser.add_argument(
|
||||||
default=None, help=cls.config_file.__doc__)
|
'--config',
|
||||||
parser.add_argument('--pidfile', '-P', dest='pidfile', required=False,
|
'-c',
|
||||||
default=None, help="File where platypush will " +
|
dest='config',
|
||||||
"store its PID, useful if you're planning to " +
|
required=False,
|
||||||
"integrate it in a service")
|
default=None,
|
||||||
parser.add_argument('--no-capture-stdout', dest='no_capture_stdout',
|
help=cls.config_file.__doc__,
|
||||||
required=False, action='store_true',
|
)
|
||||||
help="Set this flag if you have max stack depth " +
|
parser.add_argument(
|
||||||
"exceeded errors so stdout won't be captured by " +
|
'--pidfile',
|
||||||
"the logging system")
|
'-P',
|
||||||
parser.add_argument('--no-capture-stderr', dest='no_capture_stderr',
|
dest='pidfile',
|
||||||
required=False, action='store_true',
|
required=False,
|
||||||
help="Set this flag if you have max stack depth " +
|
default=None,
|
||||||
"exceeded errors so stderr won't be captured by " +
|
help="File where platypush will "
|
||||||
"the logging system")
|
+ "store its PID, useful if you're planning to "
|
||||||
parser.add_argument('--redis-queue', dest='redis_queue',
|
+ "integrate it in a service",
|
||||||
required=False, action='store_true',
|
)
|
||||||
default=cls._default_redis_queue,
|
parser.add_argument(
|
||||||
help="Name of the Redis queue to be used to internally deliver messages "
|
'--no-capture-stdout',
|
||||||
"(default: platypush/bus)")
|
dest='no_capture_stdout',
|
||||||
|
required=False,
|
||||||
|
action='store_true',
|
||||||
|
help="Set this flag if you have max stack depth "
|
||||||
|
+ "exceeded errors so stdout won't be captured by "
|
||||||
|
+ "the logging system",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-capture-stderr',
|
||||||
|
dest='no_capture_stderr',
|
||||||
|
required=False,
|
||||||
|
action='store_true',
|
||||||
|
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,
|
||||||
|
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)
|
opts, args = parser.parse_known_args(args)
|
||||||
return cls(config_file=opts.config, pidfile=opts.pidfile,
|
return cls(
|
||||||
no_capture_stdout=opts.no_capture_stdout,
|
config_file=opts.config,
|
||||||
no_capture_stderr=opts.no_capture_stderr,
|
pidfile=opts.pidfile,
|
||||||
redis_queue=opts.redis_queue)
|
no_capture_stdout=opts.no_capture_stdout,
|
||||||
|
no_capture_stderr=opts.no_capture_stderr,
|
||||||
|
redis_queue=opts.redis_queue,
|
||||||
|
)
|
||||||
|
|
||||||
def on_message(self):
|
def on_message(self):
|
||||||
"""
|
"""
|
||||||
|
@ -145,8 +181,10 @@ class Daemon:
|
||||||
logger.info('Dropped unauthorized request: {}'.format(msg))
|
logger.info('Dropped unauthorized request: {}'.format(msg))
|
||||||
|
|
||||||
self.processed_requests += 1
|
self.processed_requests += 1
|
||||||
if self.requests_to_process \
|
if (
|
||||||
and self.processed_requests >= self.requests_to_process:
|
self.requests_to_process
|
||||||
|
and self.processed_requests >= self.requests_to_process
|
||||||
|
):
|
||||||
self.stop_app()
|
self.stop_app()
|
||||||
elif isinstance(msg, Response):
|
elif isinstance(msg, Response):
|
||||||
logger.info('Received response: {}'.format(msg))
|
logger.info('Received response: {}'.format(msg))
|
||||||
|
@ -158,7 +196,7 @@ class Daemon:
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
def stop_app(self):
|
def stop_app(self):
|
||||||
""" Stops the backends and the bus """
|
"""Stops the backends and the bus"""
|
||||||
from .plugins import RunnablePlugin
|
from .plugins import RunnablePlugin
|
||||||
|
|
||||||
for backend in self.backends.values():
|
for backend in self.backends.values():
|
||||||
|
@ -173,7 +211,7 @@ class Daemon:
|
||||||
self.cron_scheduler.stop()
|
self.cron_scheduler.stop()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
""" Start the daemon """
|
"""Start the daemon"""
|
||||||
if not self.no_capture_stdout:
|
if not self.no_capture_stdout:
|
||||||
sys.stdout = Logger(logger.info)
|
sys.stdout = Logger(logger.info)
|
||||||
if not self.no_capture_stderr:
|
if not self.no_capture_stderr:
|
||||||
|
|
|
@ -99,6 +99,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
if self._is_expected_response(msg):
|
if self._is_expected_response(msg):
|
||||||
# Expected response, trigger the response handler
|
# Expected response, trigger the response handler
|
||||||
clear_timeout()
|
clear_timeout()
|
||||||
|
# pylint: disable=unsubscriptable-object
|
||||||
self._request_context['on_response'](msg)
|
self._request_context['on_response'](msg)
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
|
@ -110,12 +111,13 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
""" Internal only - returns true if we are expecting for a response
|
""" Internal only - returns true if we are expecting for a response
|
||||||
and msg is that response """
|
and msg is that response """
|
||||||
|
|
||||||
|
# pylint: disable=unsubscriptable-object
|
||||||
return self._request_context \
|
return self._request_context \
|
||||||
and isinstance(msg, Response) \
|
and isinstance(msg, Response) \
|
||||||
and msg.id == self._request_context['request'].id
|
and msg.id == self._request_context['request'].id
|
||||||
|
|
||||||
def _get_backend_config(self):
|
def _get_backend_config(self):
|
||||||
config_name = 'backend.' + self.__class__.__name__.split('Backend')[0].lower()
|
config_name = 'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower()
|
||||||
return Config.get(config_name)
|
return Config.get(config_name)
|
||||||
|
|
||||||
def _setup_response_handler(self, request, on_response, response_timeout):
|
def _setup_response_handler(self, request, on_response, response_timeout):
|
||||||
|
@ -196,7 +198,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
|
|
||||||
self.send_message(response, **kwargs)
|
self.send_message(response, **kwargs)
|
||||||
|
|
||||||
def send_message(self, msg, queue_name=None, **kwargs):
|
def send_message(self, msg, queue_name=None, **_):
|
||||||
"""
|
"""
|
||||||
Sends a platypush.message.Message to a node.
|
Sends a platypush.message.Message to a node.
|
||||||
To be implemented in the derived classes. By default, if the Redis
|
To be implemented in the derived classes. By default, if the Redis
|
||||||
|
@ -213,8 +215,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
if not redis:
|
if not redis:
|
||||||
raise KeyError()
|
raise KeyError()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.logger.warning("Backend {} does not implement send_message " +
|
self.logger.warning((
|
||||||
"and the fallback Redis backend isn't configured")
|
"Backend {} does not implement send_message "
|
||||||
|
"and the fallback Redis backend isn't configured"
|
||||||
|
).format(self.__class__.__name__))
|
||||||
return
|
return
|
||||||
|
|
||||||
redis.send_message(msg, queue_name=queue_name)
|
redis.send_message(msg, queue_name=queue_name)
|
||||||
|
@ -233,6 +237,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
|
|
||||||
while not self.should_stop() and not has_error:
|
while not self.should_stop() and not has_error:
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=not-callable
|
||||||
self.loop()
|
self.loop()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
has_error = True
|
has_error = True
|
||||||
|
@ -259,7 +264,6 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
""" Callback invoked when the process stops """
|
""" Callback invoked when the process stops """
|
||||||
pass
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stops the backend thread by sending a STOP event on its bus """
|
""" Stops the backend thread by sending a STOP event on its bus """
|
||||||
|
@ -281,7 +285,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
|
|
||||||
redis_backend = get_backend('redis')
|
redis_backend = get_backend('redis')
|
||||||
if not redis_backend:
|
if not redis_backend:
|
||||||
self.logger.warning('Redis backend not configured - some ' +
|
self.logger.warning('Redis backend not configured - some '
|
||||||
'web server features may not be working properly')
|
'web server features may not be working properly')
|
||||||
redis_args = {}
|
redis_args = {}
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -84,7 +84,9 @@ class BluetoothScannerBackend(SensorBackend):
|
||||||
with self._bt_lock:
|
with self._bt_lock:
|
||||||
return super().get_measurement()
|
return super().get_measurement()
|
||||||
|
|
||||||
def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]):
|
def process_data( # lgtm [py/inheritance/signature-mismatch]
|
||||||
|
self, data: Dict[str, dict], new_data: Optional[Dict[str, dict]] = None, **_
|
||||||
|
):
|
||||||
for addr, dev in data.items():
|
for addr, dev in data.items():
|
||||||
self._add_last_seen_device(dev)
|
self._add_last_seen_device(dev)
|
||||||
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import time
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import pyperclip
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.message.event.clipboard import ClipboardEvent
|
|
||||||
|
|
||||||
|
|
||||||
class ClipboardBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend monitors for changes in the clipboard and generates even when the user copies a new text.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
- **pyperclip** (``pip install pyperclip``)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- :class:`platypush.message.event.clipboard.ClipboardEvent` on clipboard update.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
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:
|
|
||||||
self.bus.post(ClipboardEvent(text=text))
|
|
||||||
|
|
||||||
self._last_text = text
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
self.logger.info('Stopped clipboard monitor backend')
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,8 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.clipboard.ClipboardEvent: on clipboard update.
|
|
||||||
install:
|
|
||||||
pip:
|
|
||||||
- pyperclip
|
|
||||||
package: platypush.backend.clipboard
|
|
||||||
type: backend
|
|
|
@ -1,86 +0,0 @@
|
||||||
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:
|
|
|
@ -1,7 +0,0 @@
|
||||||
manifest:
|
|
||||||
events: {}
|
|
||||||
install:
|
|
||||||
pip:
|
|
||||||
- dbus-python
|
|
||||||
package: platypush.backend.dbus
|
|
||||||
type: backend
|
|
|
@ -33,10 +33,10 @@ class FileMonitorBackend(Backend):
|
||||||
if isinstance(resource, str):
|
if isinstance(resource, str):
|
||||||
resource = MonitoredResource(resource)
|
resource = MonitoredResource(resource)
|
||||||
elif isinstance(resource, dict):
|
elif isinstance(resource, dict):
|
||||||
if 'patterns' in resource or 'ignore_patterns' in resource:
|
if 'regexes' in resource or 'ignore_regexes' in resource:
|
||||||
resource = MonitoredPattern(**resource)
|
|
||||||
elif 'regexes' in resource or 'ignore_regexes' in resource:
|
|
||||||
resource = MonitoredRegex(**resource)
|
resource = MonitoredRegex(**resource)
|
||||||
|
elif 'patterns' in resource or 'ignore_patterns' in resource or 'ignore_directories' in resource:
|
||||||
|
resource = MonitoredPattern(**resource)
|
||||||
else:
|
else:
|
||||||
resource = MonitoredResource(**resource)
|
resource = MonitoredResource(**resource)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler, RegexMatchingEventHandler
|
from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler, RegexMatchingEventHandler
|
||||||
|
|
||||||
|
@ -16,14 +17,50 @@ class EventHandler(FileSystemEventHandler):
|
||||||
resource.path = os.path.expanduser(resource.path)
|
resource.path = os.path.expanduser(resource.path)
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
|
|
||||||
|
def _should_ignore_event(self, event) -> bool:
|
||||||
|
ignore_dirs = [
|
||||||
|
os.path.expanduser(
|
||||||
|
_dir if os.path.expanduser(_dir).strip('/').startswith(self.resource.path)
|
||||||
|
else os.path.join(self.resource.path, _dir)
|
||||||
|
)
|
||||||
|
for _dir in getattr(self.resource, 'ignore_directories', [])
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore_patterns = getattr(self.resource, 'ignore_patterns', None)
|
||||||
|
ignore_regexes = getattr(self.resource, 'ignore_regexes', None)
|
||||||
|
|
||||||
|
if ignore_dirs and any(
|
||||||
|
event.src_path.startswith(ignore_dir) for ignore_dir in ignore_dirs
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ignore_patterns and any(
|
||||||
|
re.match(r'^{}$'.format(pattern.replace('*', '.*')), event.src_path)
|
||||||
|
for pattern in ignore_patterns
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ignore_regexes and any(
|
||||||
|
re.match(regex, event.src_path)
|
||||||
|
for regex in ignore_patterns
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_event(self, event, output_event_type):
|
||||||
|
if self._should_ignore_event(event):
|
||||||
|
return
|
||||||
|
get_bus().post(output_event_type(path=event.src_path, is_directory=event.is_directory))
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
get_bus().post(FileSystemCreateEvent(path=event.src_path, is_directory=event.is_directory))
|
self._on_event(event, FileSystemCreateEvent)
|
||||||
|
|
||||||
def on_deleted(self, event):
|
def on_deleted(self, event):
|
||||||
get_bus().post(FileSystemDeleteEvent(path=event.src_path, is_directory=event.is_directory))
|
self._on_event(event, FileSystemDeleteEvent)
|
||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
get_bus().post(FileSystemModifyEvent(path=event.src_path, is_directory=event.is_directory))
|
self._on_event(event, FileSystemModifyEvent)
|
||||||
|
|
||||||
def on_moved(self, event):
|
def on_moved(self, event):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -4,7 +4,6 @@ import threading
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
import pytz
|
|
||||||
import requests
|
import requests
|
||||||
from sqlalchemy import create_engine, Column, String, DateTime
|
from sqlalchemy import create_engine, Column, String, DateTime
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
@ -129,7 +128,7 @@ class GithubBackend(Backend):
|
||||||
def _get_last_event_time(self, uri: str):
|
def _get_last_event_time(self, uri: str):
|
||||||
with self.db_lock:
|
with self.db_lock:
|
||||||
record = self._get_or_create_resource(uri=uri, session=Session())
|
record = self._get_or_create_resource(uri=uri, session=Session())
|
||||||
return record.last_updated_at.replace(tzinfo=pytz.UTC) if record.last_updated_at else None
|
return record.last_updated_at.replace(tzinfo=datetime.timezone.utc) if record.last_updated_at else None
|
||||||
|
|
||||||
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
|
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
|
||||||
with self.db_lock:
|
with self.db_lock:
|
||||||
|
|
|
@ -91,14 +91,16 @@ class HttpBackend(Backend):
|
||||||
other music plugin enabled. -->
|
other music plugin enabled. -->
|
||||||
<Music class="col-3" />
|
<Music class="col-3" />
|
||||||
|
|
||||||
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
|
<!-- Show current date, time and weather.
|
||||||
|
It requires a `weather` plugin or backend enabled -->
|
||||||
<DateTimeWeather class="col-3" />
|
<DateTimeWeather class="col-3" />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- Display the following widgets on a second row -->
|
<!-- Display the following widgets on a second row -->
|
||||||
<Row>
|
<Row>
|
||||||
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
<!-- 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. -->
|
explicitly exposed as an HTTP resource through the backend
|
||||||
|
`resource_dirs` attribute. -->
|
||||||
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
<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
|
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
||||||
|
@ -151,34 +153,41 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **flask** (``pip install flask``)
|
* **gunicorn** (``pip install gunicorn``) - optional, to run the Platypush webapp over uWSGI.
|
||||||
* **bcrypt** (``pip install bcrypt``)
|
|
||||||
* **magic** (``pip install python-magic``), optional, for MIME type
|
|
||||||
support if you want to enable media streaming
|
|
||||||
* **uwsgi** (``pip install uwsgi`` plus uwsgi server installed on your
|
|
||||||
system if required) - optional but recommended. By default the
|
|
||||||
Platypush web server will run in a process spawned on the fly by
|
|
||||||
the HTTP backend. However, being a Flask app, it will serve clients
|
|
||||||
in a single thread and won't support many features of a full-blown
|
|
||||||
web server.
|
|
||||||
|
|
||||||
Base command to run the web server over uwsgi::
|
By default the Platypush web server will run in a
|
||||||
|
process spawned on the fly by the HTTP backend. However, being a
|
||||||
|
Flask app, it will serve clients in a single thread and it won't
|
||||||
|
support many features of a full-blown web server. gunicorn allows
|
||||||
|
you to easily spawn the web server in a uWSGI wrapper, separate
|
||||||
|
from the main Platypush daemon, and the uWSGI layer can be easily
|
||||||
|
exposed over an nginx/lighttpd web server.
|
||||||
|
|
||||||
uwsgi --http :8008 --module platypush.backend.http.uwsgi --master --processes 4 --threads 4
|
Command to run the web server over a gunicorn uWSGI wrapper::
|
||||||
|
|
||||||
|
gunicorn -w <n_workers> -b <bind_address>:8008 platypush.backend.http.uwsgi
|
||||||
|
|
||||||
Bear in mind that the main webapp is defined in ``platypush.backend.http.app:application``
|
|
||||||
and the WSGI startup script is stored under ``platypush/backend/http/uwsgi.py``.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_DEFAULT_HTTP_PORT = 8008
|
_DEFAULT_HTTP_PORT = 8008
|
||||||
_DEFAULT_WEBSOCKET_PORT = 8009
|
_DEFAULT_WEBSOCKET_PORT = 8009
|
||||||
|
|
||||||
def __init__(self, port=_DEFAULT_HTTP_PORT,
|
def __init__(
|
||||||
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
self,
|
||||||
bind_address='0.0.0.0',
|
port=_DEFAULT_HTTP_PORT,
|
||||||
disable_websocket=False, resource_dirs=None,
|
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
||||||
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None,
|
bind_address='0.0.0.0',
|
||||||
maps=None, run_externally=False, uwsgi_args=None, **kwargs):
|
disable_websocket=False,
|
||||||
|
resource_dirs=None,
|
||||||
|
ssl_cert=None,
|
||||||
|
ssl_key=None,
|
||||||
|
ssl_cafile=None,
|
||||||
|
ssl_capath=None,
|
||||||
|
maps=None,
|
||||||
|
run_externally=False,
|
||||||
|
uwsgi_args=None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param port: Listen port for the web server (default: 8008)
|
:param port: Listen port for the web server (default: 8008)
|
||||||
:type port: int
|
:type port: int
|
||||||
|
@ -245,26 +254,37 @@ class HttpBackend(Backend):
|
||||||
self.bind_address = bind_address
|
self.bind_address = bind_address
|
||||||
|
|
||||||
if resource_dirs:
|
if resource_dirs:
|
||||||
self.resource_dirs = {name: os.path.abspath(
|
self.resource_dirs = {
|
||||||
os.path.expanduser(d)) for name, d in resource_dirs.items()}
|
name: os.path.abspath(os.path.expanduser(d))
|
||||||
|
for name, d in resource_dirs.items()
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
self.resource_dirs = {}
|
self.resource_dirs = {}
|
||||||
|
|
||||||
self.active_websockets = set()
|
self.active_websockets = set()
|
||||||
self.run_externally = run_externally
|
self.run_externally = run_externally
|
||||||
self.uwsgi_args = uwsgi_args or []
|
self.uwsgi_args = uwsgi_args or []
|
||||||
self.ssl_context = get_ssl_server_context(ssl_cert=ssl_cert,
|
self.ssl_context = (
|
||||||
ssl_key=ssl_key,
|
get_ssl_server_context(
|
||||||
ssl_cafile=ssl_cafile,
|
ssl_cert=ssl_cert,
|
||||||
ssl_capath=ssl_capath) \
|
ssl_key=ssl_key,
|
||||||
if ssl_cert else None
|
ssl_cafile=ssl_cafile,
|
||||||
|
ssl_capath=ssl_capath,
|
||||||
|
)
|
||||||
|
if ssl_cert
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
if self.uwsgi_args:
|
if self.uwsgi_args:
|
||||||
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \
|
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [
|
||||||
['--module', 'platypush.backend.http.uwsgi', '--enable-threads']
|
'--module',
|
||||||
|
'platypush.backend.http.uwsgi',
|
||||||
|
'--enable-threads',
|
||||||
|
]
|
||||||
|
|
||||||
self.local_base_url = '{proto}://localhost:{port}'.\
|
self.local_base_url = '{proto}://localhost:{port}'.format(
|
||||||
format(proto=('https' if ssl_cert else 'http'), port=self.port)
|
proto=('https' if ssl_cert else 'http'), port=self.port
|
||||||
|
)
|
||||||
|
|
||||||
self._websocket_lock_timeout = 10
|
self._websocket_lock_timeout = 10
|
||||||
self._websocket_lock = threading.RLock()
|
self._websocket_lock = threading.RLock()
|
||||||
|
@ -274,7 +294,7 @@ class HttpBackend(Backend):
|
||||||
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
|
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
""" On backend stop """
|
"""On backend stop"""
|
||||||
super().on_stop()
|
super().on_stop()
|
||||||
self.logger.info('Received STOP event on HttpBackend')
|
self.logger.info('Received STOP event on HttpBackend')
|
||||||
|
|
||||||
|
@ -283,7 +303,9 @@ class HttpBackend(Backend):
|
||||||
self.server_proc.kill()
|
self.server_proc.kill()
|
||||||
self.server_proc.wait(timeout=10)
|
self.server_proc.wait(timeout=10)
|
||||||
if self.server_proc.poll() is not None:
|
if self.server_proc.poll() is not None:
|
||||||
self.logger.info('HTTP server process may be still alive at termination')
|
self.logger.info(
|
||||||
|
'HTTP server process may be still alive at termination'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.info('HTTP server process terminated')
|
self.logger.info('HTTP server process terminated')
|
||||||
else:
|
else:
|
||||||
|
@ -292,17 +314,25 @@ class HttpBackend(Backend):
|
||||||
if self.server_proc.is_alive():
|
if self.server_proc.is_alive():
|
||||||
self.server_proc.kill()
|
self.server_proc.kill()
|
||||||
if self.server_proc.is_alive():
|
if self.server_proc.is_alive():
|
||||||
self.logger.info('HTTP server process may be still alive at termination')
|
self.logger.info(
|
||||||
|
'HTTP server process may be still alive at termination'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.info('HTTP server process terminated')
|
self.logger.info('HTTP server process terminated')
|
||||||
|
|
||||||
if self.websocket_thread and self.websocket_thread.is_alive() and self._websocket_loop:
|
if (
|
||||||
|
self.websocket_thread
|
||||||
|
and self.websocket_thread.is_alive()
|
||||||
|
and self._websocket_loop
|
||||||
|
):
|
||||||
self._websocket_loop.stop()
|
self._websocket_loop.stop()
|
||||||
self.logger.info('HTTP websocket service terminated')
|
self.logger.info('HTTP websocket service terminated')
|
||||||
|
|
||||||
def _acquire_websocket_lock(self, ws):
|
def _acquire_websocket_lock(self, ws):
|
||||||
try:
|
try:
|
||||||
acquire_ok = self._websocket_lock.acquire(timeout=self._websocket_lock_timeout)
|
acquire_ok = self._websocket_lock.acquire(
|
||||||
|
timeout=self._websocket_lock_timeout
|
||||||
|
)
|
||||||
if not acquire_ok:
|
if not acquire_ok:
|
||||||
raise TimeoutError('Websocket lock acquire timeout')
|
raise TimeoutError('Websocket lock acquire timeout')
|
||||||
|
|
||||||
|
@ -312,13 +342,19 @@ class HttpBackend(Backend):
|
||||||
finally:
|
finally:
|
||||||
self._websocket_lock.release()
|
self._websocket_lock.release()
|
||||||
|
|
||||||
acquire_ok = self._websocket_locks[addr].acquire(timeout=self._websocket_lock_timeout)
|
acquire_ok = self._websocket_locks[addr].acquire(
|
||||||
|
timeout=self._websocket_lock_timeout
|
||||||
|
)
|
||||||
if not acquire_ok:
|
if not acquire_ok:
|
||||||
raise TimeoutError('Websocket on address {} not ready to receive data'.format(addr))
|
raise TimeoutError(
|
||||||
|
'Websocket on address {} not ready to receive data'.format(addr)
|
||||||
|
)
|
||||||
|
|
||||||
def _release_websocket_lock(self, ws):
|
def _release_websocket_lock(self, ws):
|
||||||
try:
|
try:
|
||||||
acquire_ok = self._websocket_lock.acquire(timeout=self._websocket_lock_timeout)
|
acquire_ok = self._websocket_lock.acquire(
|
||||||
|
timeout=self._websocket_lock_timeout
|
||||||
|
)
|
||||||
if not acquire_ok:
|
if not acquire_ok:
|
||||||
raise TimeoutError('Websocket lock acquire timeout')
|
raise TimeoutError('Websocket lock acquire timeout')
|
||||||
|
|
||||||
|
@ -326,12 +362,15 @@ class HttpBackend(Backend):
|
||||||
if addr in self._websocket_locks:
|
if addr in self._websocket_locks:
|
||||||
self._websocket_locks[addr].release()
|
self._websocket_locks[addr].release()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Unhandled exception while releasing websocket lock: {}'.format(str(e)))
|
self.logger.warning(
|
||||||
|
'Unhandled exception while releasing websocket lock: {}'.format(str(e))
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
self._websocket_lock.release()
|
self._websocket_lock.release()
|
||||||
|
|
||||||
def notify_web_clients(self, event):
|
def notify_web_clients(self, event):
|
||||||
""" Notify all the connected web clients (over websocket) of a new event """
|
"""Notify all the connected web clients (over websocket) of a new event"""
|
||||||
|
|
||||||
async def send_event(ws):
|
async def send_event(ws):
|
||||||
try:
|
try:
|
||||||
self._acquire_websocket_lock(ws)
|
self._acquire_websocket_lock(ws)
|
||||||
|
@ -348,26 +387,35 @@ class HttpBackend(Backend):
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(send_event(_ws))
|
loop.run_until_complete(send_event(_ws))
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
self.logger.warning('Websocket client {} connection lost'.format(_ws.remote_address))
|
self.logger.warning(
|
||||||
|
'Websocket client {} connection lost'.format(_ws.remote_address)
|
||||||
|
)
|
||||||
self.active_websockets.remove(_ws)
|
self.active_websockets.remove(_ws)
|
||||||
if _ws.remote_address in self._websocket_locks:
|
if _ws.remote_address in self._websocket_locks:
|
||||||
del self._websocket_locks[_ws.remote_address]
|
del self._websocket_locks[_ws.remote_address]
|
||||||
|
|
||||||
def websocket(self):
|
def websocket(self):
|
||||||
""" Websocket main server """
|
"""Websocket main server"""
|
||||||
set_thread_name('WebsocketServer')
|
set_thread_name('WebsocketServer')
|
||||||
|
|
||||||
async def register_websocket(websocket, path):
|
async def register_websocket(websocket, path):
|
||||||
address = websocket.remote_address if websocket.remote_address \
|
address = (
|
||||||
|
websocket.remote_address
|
||||||
|
if websocket.remote_address
|
||||||
else '<unknown client>'
|
else '<unknown client>'
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info('New websocket connection from {} on path {}'.format(address, path))
|
self.logger.info(
|
||||||
|
'New websocket connection from {} on path {}'.format(address, path)
|
||||||
|
)
|
||||||
self.active_websockets.add(websocket)
|
self.active_websockets.add(websocket)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await websocket.recv()
|
await websocket.recv()
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
self.logger.info('Websocket client {} closed connection'.format(address))
|
self.logger.info(
|
||||||
|
'Websocket client {} closed connection'.format(address)
|
||||||
|
)
|
||||||
self.active_websockets.remove(websocket)
|
self.active_websockets.remove(websocket)
|
||||||
if address in self._websocket_locks:
|
if address in self._websocket_locks:
|
||||||
del self._websocket_locks[address]
|
del self._websocket_locks[address]
|
||||||
|
@ -378,8 +426,13 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
self._websocket_loop = get_or_create_event_loop()
|
self._websocket_loop = get_or_create_event_loop()
|
||||||
self._websocket_loop.run_until_complete(
|
self._websocket_loop.run_until_complete(
|
||||||
websocket_serve(register_websocket, self.bind_address, self.websocket_port,
|
websocket_serve(
|
||||||
**websocket_args))
|
register_websocket,
|
||||||
|
self.bind_address,
|
||||||
|
self.websocket_port,
|
||||||
|
**websocket_args
|
||||||
|
)
|
||||||
|
)
|
||||||
self._websocket_loop.run_forever()
|
self._websocket_loop.run_forever()
|
||||||
|
|
||||||
def _start_web_server(self):
|
def _start_web_server(self):
|
||||||
|
@ -402,7 +455,11 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
self.register_service(port=self.port)
|
try:
|
||||||
|
self.register_service(port=self.port)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Could not register the Zeroconf service')
|
||||||
|
self.logger.exception(e)
|
||||||
|
|
||||||
if not self.disable_websocket:
|
if not self.disable_websocket:
|
||||||
self.logger.info('Initializing websocket interface')
|
self.logger.info('Initializing websocket interface')
|
||||||
|
@ -410,8 +467,9 @@ class HttpBackend(Backend):
|
||||||
self.websocket_thread.start()
|
self.websocket_thread.start()
|
||||||
|
|
||||||
if not self.run_externally:
|
if not self.run_externally:
|
||||||
self.server_proc = Process(target=self._start_web_server(),
|
self.server_proc = Process(
|
||||||
name='WebServer')
|
target=self._start_web_server(), name='WebServer'
|
||||||
|
)
|
||||||
self.server_proc.start()
|
self.server_proc.start()
|
||||||
self.server_proc.join()
|
self.server_proc.join()
|
||||||
elif self.uwsgi_args:
|
elif self.uwsgi_args:
|
||||||
|
@ -419,9 +477,11 @@ class HttpBackend(Backend):
|
||||||
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
|
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
|
||||||
self.server_proc = subprocess.Popen(uwsgi_cmd)
|
self.server_proc = subprocess.Popen(uwsgi_cmd)
|
||||||
else:
|
else:
|
||||||
self.logger.info('The web server is configured to be launched externally but ' +
|
self.logger.info(
|
||||||
'no uwsgi_args were provided. Make sure that you run another external service' +
|
'The web server is configured to be launched externally but '
|
||||||
'for the webserver (e.g. nginx)')
|
+ 'no uwsgi_args were provided. Make sure that you run another external service'
|
||||||
|
+ 'for the webserver (e.g. nginx)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
from flask import Blueprint, request, abort, jsonify
|
from flask import Blueprint, request, abort, jsonify
|
||||||
|
|
||||||
|
@ -18,7 +17,7 @@ __routes__ = [
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/auth', methods=['POST'])
|
@auth.route('/auth', methods=['POST'])
|
||||||
def auth_endpoint() -> Dict[str, Optional[str]]:
|
def auth_endpoint():
|
||||||
"""
|
"""
|
||||||
Authentication endpoint. It validates the user credentials provided over a JSON payload with the following
|
Authentication endpoint. It validates the user credentials provided over a JSON payload with the following
|
||||||
structure:
|
structure:
|
||||||
|
|
|
@ -16,12 +16,14 @@ __routes__ = [
|
||||||
@execute.route('/execute', methods=['POST'])
|
@execute.route('/execute', methods=['POST'])
|
||||||
@authenticate()
|
@authenticate()
|
||||||
def execute():
|
def execute():
|
||||||
""" Endpoint to execute commands """
|
"""Endpoint to execute commands"""
|
||||||
try:
|
try:
|
||||||
msg = json.loads(request.data.decode('utf-8'))
|
msg = json.loads(request.data.decode('utf-8'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger().error('Unable to parse JSON from request {}: {}'.format(request.data, str(e)))
|
logger().error(
|
||||||
return abort(400, str(e))
|
'Unable to parse JSON from request {}: {}'.format(request.data, str(e))
|
||||||
|
)
|
||||||
|
abort(400, str(e))
|
||||||
|
|
||||||
logger().info('Received message on the HTTP backend: {}'.format(msg))
|
logger().info('Received message on the HTTP backend: {}'.format(msg))
|
||||||
|
|
||||||
|
@ -29,8 +31,10 @@ def execute():
|
||||||
response = send_message(msg)
|
response = send_message(msg)
|
||||||
return Response(str(response or {}), mimetype='application/json')
|
return Response(str(response or {}), mimetype='application/json')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger().error('Error while running HTTP action: {}. Request: {}'.format(str(e), msg))
|
logger().error(
|
||||||
return abort(500, str(e))
|
'Error while running HTTP action: {}. Request: {}'.format(str(e), msg)
|
||||||
|
)
|
||||||
|
abort(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -13,17 +13,19 @@ __routes__ = [
|
||||||
|
|
||||||
@logout.route('/logout', methods=['GET', 'POST'])
|
@logout.route('/logout', methods=['GET', 'POST'])
|
||||||
def logout():
|
def logout():
|
||||||
""" Logout page """
|
"""Logout page"""
|
||||||
user_manager = UserManager()
|
user_manager = UserManager()
|
||||||
redirect_page = request.args.get('redirect', request.headers.get('Referer', '/login'))
|
redirect_page = request.args.get(
|
||||||
|
'redirect', request.headers.get('Referer', '/login')
|
||||||
|
)
|
||||||
session_token = request.cookies.get('session_token')
|
session_token = request.cookies.get('session_token')
|
||||||
|
|
||||||
if not session_token:
|
if not session_token:
|
||||||
return abort(417, 'Not logged in')
|
abort(417, 'Not logged in')
|
||||||
|
|
||||||
user, session = user_manager.authenticate_user_session(session_token)
|
user, _ = user_manager.authenticate_user_session(session_token)
|
||||||
if not user:
|
if not user:
|
||||||
return abort(403, 'Invalid session token')
|
abort(403, 'Invalid session token')
|
||||||
|
|
||||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||||
response = make_response(redirect_target)
|
response = make_response(redirect_target)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import json
|
import json
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Response, Blueprint, request
|
from flask import Blueprint, request
|
||||||
|
from flask.wrappers import Response
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
from platypush.backend.http.app import template_folder
|
||||||
from platypush.backend.http.app.utils import authenticate
|
from platypush.backend.http.app.utils import authenticate
|
||||||
|
@ -17,19 +18,22 @@ __routes__ = [
|
||||||
|
|
||||||
|
|
||||||
def get_camera(plugin: str) -> CameraPlugin:
|
def get_camera(plugin: str) -> CameraPlugin:
|
||||||
return get_plugin('camera.' + plugin)
|
plugin_name = f'camera.{plugin}'
|
||||||
|
p = get_plugin(plugin_name)
|
||||||
|
assert p, f'No such plugin: {plugin_name}'
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
def get_frame(session: Camera, timeout: Optional[float] = None) -> bytes:
|
def get_frame(session: Camera, timeout: Optional[float] = None) -> Optional[bytes]:
|
||||||
with session.stream.ready:
|
if session.stream:
|
||||||
session.stream.ready.wait(timeout=timeout)
|
with session.stream.ready:
|
||||||
return session.stream.frame
|
session.stream.ready.wait(timeout=timeout)
|
||||||
|
return session.stream.frame
|
||||||
|
|
||||||
|
|
||||||
def feed(plugin: str, **kwargs):
|
def feed(camera: CameraPlugin, **kwargs):
|
||||||
plugin = get_camera(plugin)
|
with camera.open(**kwargs) as session:
|
||||||
with plugin.open(stream=True, **kwargs) as session:
|
camera.start_camera(session)
|
||||||
plugin.start_camera(session)
|
|
||||||
while True:
|
while True:
|
||||||
frame = get_frame(session, timeout=5.0)
|
frame = get_frame(session, timeout=5.0)
|
||||||
if frame:
|
if frame:
|
||||||
|
@ -77,8 +81,12 @@ def get_photo(plugin, extension):
|
||||||
@authenticate()
|
@authenticate()
|
||||||
def get_video(plugin, extension):
|
def get_video(plugin, extension):
|
||||||
stream_class = StreamWriter.get_class_by_name(extension)
|
stream_class = StreamWriter.get_class_by_name(extension)
|
||||||
return Response(feed(plugin, stream_format=extension, frames_dir=None, **get_args(request.args)),
|
camera = get_camera(plugin)
|
||||||
mimetype=stream_class.mimetype)
|
return Response(
|
||||||
|
feed(camera, stream=True, stream_format=extension, frames_dir=None,
|
||||||
|
**get_args(request.args)
|
||||||
|
), mimetype=stream_class.mimetype
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@camera.route('/camera/<plugin>/photo', methods=['GET'])
|
@camera.route('/camera/<plugin>/photo', methods=['GET'])
|
||||||
|
|
|
@ -41,8 +41,11 @@ def resources_path(path):
|
||||||
real_base_path = os.path.abspath(os.path.expanduser(resource_dirs[base_path]))
|
real_base_path = os.path.abspath(os.path.expanduser(resource_dirs[base_path]))
|
||||||
real_path = real_base_path
|
real_path = real_base_path
|
||||||
|
|
||||||
file_path = [s for s in re.sub(r'^{}(.*)$'.format(base_path), '\\1', path)
|
file_path = [
|
||||||
.split('/') if s]
|
s for s in re.sub(
|
||||||
|
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
|
||||||
|
).split('/') if s
|
||||||
|
]
|
||||||
|
|
||||||
for p in file_path[:-1]:
|
for p in file_path[:-1]:
|
||||||
real_path += os.sep + p
|
real_path += os.sep + p
|
||||||
|
|
|
@ -2,8 +2,6 @@ manifest:
|
||||||
events: {}
|
events: {}
|
||||||
install:
|
install:
|
||||||
pip:
|
pip:
|
||||||
- flask
|
- gunicorn
|
||||||
- bcrypt
|
|
||||||
- python-magic
|
|
||||||
package: platypush.backend.http
|
package: platypush.backend.http
|
||||||
type: backend
|
type: backend
|
||||||
|
|
|
@ -7,6 +7,9 @@ from platypush.backend.http.request import HttpRequest
|
||||||
|
|
||||||
class HttpPollBackend(Backend):
|
class HttpPollBackend(Backend):
|
||||||
"""
|
"""
|
||||||
|
WARNING: This integration is deprecated, since it was practically only used for RSS subscriptions.
|
||||||
|
RSS feeds integration has been replaced by :class:`platypush.plugins.rss.RSSPlugin`.
|
||||||
|
|
||||||
This backend will poll multiple HTTP endpoints/services and return events
|
This backend will poll multiple HTTP endpoints/services and return events
|
||||||
the bus whenever something new happened. Supported types:
|
the bus whenever something new happened. Supported types:
|
||||||
:class:`platypush.backend.http.request.JsonHttpRequest` (for polling updates on
|
:class:`platypush.backend.http.request.JsonHttpRequest` (for polling updates on
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
],
|
||||||
|
plugins: [],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 0,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-label="Jellyfin" role="img" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="15%" fill="#fff" fill-opacity="0"/>
|
||||||
|
<defs>
|
||||||
|
<path d="M190.56 329.07c8.63 17.3 122.4 17.12 130.93 0 8.52-17.1-47.9-119.78-65.46-119.8-17.57 0-74.1 102.5-65.47 119.8z" id="A"/>
|
||||||
|
<linearGradient id="B" gradientUnits="userSpaceOnUse" x1="126.15" y1="219.32" x2="457.68" y2="410.73">
|
||||||
|
<stop offset="0%" stop-color="#aa5cc3"/>
|
||||||
|
<stop offset="100%" stop-color="#00a4dc"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M58.75 417.03c25.97 52.15 368.86 51.55 394.55 0S308.93 56.08 256.03 56.08c-52.92 0-223.25 308.8-197.28 360.95zm68.04-45.25c-17.02-34.17 94.6-236.5 129.26-236.5 34.67 0 146.1 202.7 129.26 236.5-16.83 33.8-241.5 34.17-258.52 0z" id="C"/>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<use xlink:href="#A" fill="url(#B)"/>
|
||||||
|
<use xlink:href="#A" fill-opacity="0" stroke="#000" stroke-opacity="0"/>
|
||||||
|
<use xlink:href="#C" fill="url(#B)"/>
|
||||||
|
<use xlink:href="#C" fill-opacity="0" stroke="#000" stroke-opacity="0"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue