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
|
||||
/platypush/backend/http/static/css/dist
|
||||
/tests/etc/dashboards
|
||||
.coverage
|
||||
coverage.xml
|
||||
Session.vim
|
||||
|
|
|
@ -56,11 +56,17 @@ upload-pip-package:
|
|||
only:
|
||||
- tags
|
||||
script:
|
||||
# Update the CI/CD configuration
|
||||
- cd ~/platypush-ci-cd
|
||||
- git pull
|
||||
- cd -
|
||||
# Build the package
|
||||
- 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/')
|
||||
- source ~/.credentials/pypi.env
|
||||
- python setup.py sdist bdist_wheel
|
||||
# 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
|
||||
- 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.
|
||||
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
|
||||
|
||||
## Added
|
||||
### Added
|
||||
|
||||
- `gotify` integration (see #198).
|
||||
|
||||
## [0.22.2] - 2021-09-25
|
||||
|
||||
## Added
|
||||
### Added
|
||||
|
||||
- `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.
|
||||
|
||||
## Changed
|
||||
### Changed
|
||||
|
||||
- Created CI Gitlab pipeline to replace the Platypush event-based pre-existing pipeline.
|
||||
|
||||
## Removed
|
||||
### Removed
|
||||
|
||||
- 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.
|
||||
|
||||
- `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
|
||||
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
|
||||
directly accessing the device's Bluetooth interface.
|
||||
|
||||
|
||||
- Added `marshmallow` dependency - it will be used from now own to dump and document schemas
|
||||
and responses instead of the currently mixed approach with `Response` objects and plain
|
||||
dictionaries and lists.
|
||||
|
||||
|
||||
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
|
||||
|
||||
- Added generic joystick backend `backend.joystick.jstest` which uses `jstest` from the
|
||||
standard `joystick` system package to read the state of joysticks not compatible with
|
||||
`python-inputs`.
|
||||
|
||||
|
||||
- Added PWM PCA9685 plugin.
|
||||
|
||||
- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where
|
||||
|
@ -151,7 +303,7 @@ Given the high speed of development in the first phase, changes are being report
|
|||
|
||||
- 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.
|
||||
|
||||
|
@ -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
|
||||
(see [#174](https://git.platypush.tech/platypush/platypush/-/issues/174)).
|
||||
|
||||
|
||||
- Added `weather.openweathermap` plugin and backend, which replaces `weather.darksky`, since the
|
||||
Darksky API will be completely shut down by the end of 2021.
|
||||
|
||||
|
@ -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,
|
||||
not UTC, as a reference (closes [#173](https://git.platypush.tech/platypush/platypush/-/issues/173)).
|
||||
|
||||
|
||||
- Better management of Z-Wave values types from the UI.
|
||||
|
||||
- Disable logging for `ZwaveValueEvent` events - they tend to be very verbose and
|
||||
can impact the performance on slower devices. They will still be published to the
|
||||
websocket clients though, so you can still debug Z-Wave values issues from the browser
|
||||
developer console (enable debug traces).
|
||||
|
||||
|
||||
- Added suffix to the `zigbee.mqtt` backend default `client_id` to prevent clashes with
|
||||
the default `mqtt` backend `client_id`.
|
||||
|
||||
|
|
|
@ -34,6 +34,6 @@ Guidelines:
|
|||
an `extras_require` entry.
|
||||
- In the plugin/backend class pydoc string.
|
||||
- In the `manifest.yaml` - refer to the Wiki (how to write
|
||||
[plugins](https://git.platypush.tech/platypush/platypush/-/wikis/writing-your-own-plugins)
|
||||
and [backends](https://git.platypush.tech/platypush/platypush/-/wikis/writing-your-own-backends))
|
||||
[plugins](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-plugins)
|
||||
and [backends](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-backends))
|
||||
for examples on how to write an extension manifest file.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
recursive-include platypush/backend/http/webapp/dist *
|
||||
include platypush/plugins/http/webpage/mercury-parser.js
|
||||
include platypush/config/*.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/)
|
||||
[![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/)
|
||||
[![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)
|
||||
[![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)
|
||||
|
||||
<!-- 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).
|
||||
|
||||
- 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*)
|
||||
and it provides a comprehensive and customizable user interface that collects everything you need to visualize and
|
||||
control under one roof.
|
||||
It enables users to create their own self-hosted pieces of automation based on
|
||||
events (*if this happens then do that*)
|
||||
and it provides a comprehensive and customizable user interface that collects
|
||||
everything you need to visualize and control under one roof.
|
||||
|
||||
It takes some concepts from [IFTTT](https://ifttt.com), [Tasker](https://tasker.joaoapps.com/),
|
||||
[Microsoft Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
|
||||
[Home Assistant](https://www.home-assistant.io/) to provide an environment where the user can easily connect things
|
||||
together.
|
||||
It takes some concepts from [IFTTT](https://ifttt.com),
|
||||
[Tasker](https://tasker.joaoapps.com/), [Microsoft
|
||||
Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
|
||||
[Home Assistant](https://www.home-assistant.io/) to provide an environment
|
||||
where the user can easily connect things together.
|
||||
|
||||
Its ideal home is a single-board computer like a RaspberryPi that you can configure to orchestrate any home automation
|
||||
and cloud automation in your own living room or garage, but it can easily run on any device that can run a Python
|
||||
interpreter, and the bar for the hardware requirements is very low as well - I use it to run pieces of automation on
|
||||
devices as powerful as a RaspberryPi Zero or an old Nokia N900 with Linux.
|
||||
Its ideal home is a single-board computer like a RaspberryPi that you can
|
||||
configure to orchestrate any home automation and cloud automation in your own
|
||||
living room or garage, but it can easily run on any device that can run a
|
||||
Python interpreter, and the bar for the hardware requirements is very low as
|
||||
well - I use it to run pieces of automation on devices as powerful as a
|
||||
RaspberryPi Zero or an old Nokia N900 with Linux.
|
||||
|
||||
You can use Platypush to do things like:
|
||||
|
||||
|
@ -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)
|
||||
- Build integrations between [sensors](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
|
||||
[cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
||||
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html) and
|
||||
[machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html) to create smart
|
||||
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html)
|
||||
and [machine learning
|
||||
models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html)
|
||||
to create smart
|
||||
pieces of automation for e.g.
|
||||
[people detection](https://blog.platypush.tech/article/Detect-people-with-a-RaspberryPi-a-thermal-camera-Platypush-and-a-pinch-of-machine-learning)
|
||||
or [sound detection](https://blog.platypush.tech/article/Create-your-smart-baby-monitor-with-Platypush-and-Tensorflow)
|
||||
|
@ -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)
|
||||
- [Implement automated custom text-to-speech routines](https://docs.platypush.tech/en/latest/platypush/plugins/tts.html)
|
||||
- [Build any kind of interactions and automation routines with your Android device using Tasker](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
|
||||
- Play [local videos](https://docs.platypush.tech/en/latest/platypush/plugins/media.mpv.html), YouTube videos and torrent media from any device and service, to any device
|
||||
- Play [local
|
||||
videos](https://docs.platypush.tech/en/latest/platypush/plugins/media.mpv.html),
|
||||
YouTube videos and torrent media from any device and service, to any device
|
||||
- [Get weather forecast events for your location and build automation routines on them](https://docs.platypush.tech/en/latest/platypush/plugins/weather.darksky.html)
|
||||
- [Create a custom single hub for Zigbee and Z-Wave smart devices](https://blog.platypush.tech/article/Transform-a-RaspberryPi-into-a-universal-Zigbee-and-Z-Wave-bridge)
|
||||
- Build your own web dashboard with calendar, weather, news and music controls (basically, anything that has a Platypush web widget)
|
||||
- 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))
|
||||
|
||||
## Architecture
|
||||
|
||||
The architecture of Platypush consists of a few simple pieces, orchestrated by a configuration file stored by default
|
||||
under [`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml):
|
||||
The architecture of Platypush consists of a few simple pieces, orchestrated by
|
||||
a configuration file stored by default under
|
||||
[`~/.config/platypush/config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml):
|
||||
|
||||
### [Plugins](https://docs.platypush.tech/en/latest/plugins.html)
|
||||
### Plugins
|
||||
|
||||
They are integrations that do things - like
|
||||
[modify files](https://docs.platypush.tech/en/latest/platypush/plugins/file.html),
|
||||
[train and evaluate machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html),
|
||||
[control cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
||||
[read sensors](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.sensor.dht.html),
|
||||
[parse a web page](https://docs.platypush.tech/en/latest/platypush/plugins/http.webpage.html),
|
||||
[control lights](https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html),
|
||||
[send emails](https://docs.platypush.tech/en/latest/platypush/plugins/mail.smtp.html),
|
||||
[control Chromecasts](https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html),
|
||||
[run voice queries](https://docs.platypush.tech/en/latest/platypush/plugins/assistant.google.html),
|
||||
[handle torrent transfers](https://docs.platypush.tech/en/latest/platypush/plugins/torrent.html) or
|
||||
control [Zigbee](https://docs.platypush.tech/en/latest/platypush/plugins/zigbee.mqtt.html) or
|
||||
[Z-Wave](https://docs.platypush.tech/en/latest/platypush/plugins/zwave.html) devices.
|
||||
[Full list](https://docs.platypush.tech/en/latest/plugins.html)
|
||||
|
||||
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:
|
||||
Plugins are integrations that do things - like [modify
|
||||
files](https://docs.platypush.tech/en/latest/platypush/plugins/file.html),
|
||||
[train and evaluate machine learning
|
||||
models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html),
|
||||
[control
|
||||
cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
||||
[read
|
||||
sensors](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.sensor.dht.html),
|
||||
[parse a web
|
||||
page](https://docs.platypush.tech/en/latest/platypush/plugins/http.webpage.html),
|
||||
[control
|
||||
lights](https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html),
|
||||
[send
|
||||
emails](https://docs.platypush.tech/en/latest/platypush/plugins/mail.smtp.html),
|
||||
[control
|
||||
Chromecasts](https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html),
|
||||
[run voice
|
||||
queries](https://docs.platypush.tech/en/latest/platypush/plugins/assistant.google.html),
|
||||
[handle torrent
|
||||
transfers](https://docs.platypush.tech/en/latest/platypush/plugins/torrent.html)
|
||||
or control
|
||||
[Zigbee](https://docs.platypush.tech/en/latest/platypush/plugins/zigbee.mqtt.html)
|
||||
or [Z-Wave](https://docs.platypush.tech/en/latest/platypush/plugins/zwave.html)
|
||||
devices.
|
||||
|
||||
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
|
||||
light.hue:
|
||||
|
@ -100,9 +163,11 @@ light.hue:
|
|||
|
||||
### Actions
|
||||
|
||||
Plugins expose *actions*, that match one-on-one the plugin class methods denoted by `@action`, so it's very
|
||||
straightforward to invoke plugin actions by just reading the plugin documentation. They can be invoked directly from
|
||||
your own scripts or they can be sent to the platform through any supported channel as simple JSON messages:
|
||||
Plugins expose *actions*, that match one-on-one the plugin class methods
|
||||
denoted by `@action`, so it's very straightforward to invoke plugin actions by
|
||||
just reading the plugin documentation. They can be invoked directly from your
|
||||
own scripts or they can be sent to the platform through any supported channel
|
||||
as simple JSON messages:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -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
|
||||
[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.).
|
||||
[Full list](https://docs.platypush.tech/en/latest/backends.html)
|
||||
|
||||
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:
|
||||
They are background services that either listen for messages on channels (like
|
||||
an [HTTP
|
||||
backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
|
||||
[MQTT
|
||||
instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
|
||||
[Kafka
|
||||
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html),
|
||||
a [Websocket
|
||||
service](https://docs.platypush.tech/en/latest/platypush/backend/websocket.html),
|
||||
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html)
|
||||
etc.) or monitor a device or a service for events (like a
|
||||
[sensor](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
|
||||
a custom [voice
|
||||
assistant](https://docs.platypush.tech/en/latest/platypush/backend/assistant.google.html),
|
||||
a bridge running on a
|
||||
[Zigbee](https://docs.platypush.tech/en/latest/platypush/backend/zigbee.mqtt.html)
|
||||
or
|
||||
[Z-Wave](https://docs.platypush.tech/en/latest/platypush/backend/zwave.html),
|
||||
an [NFC card
|
||||
reader](https://docs.platypush.tech/en/latest/platypush/backend/nfc.html), a
|
||||
[MIDI
|
||||
device](https://docs.platypush.tech/en/latest/platypush/backend/midi.html), a
|
||||
[Telegram
|
||||
channel](https://docs.platypush.tech/en/latest/platypush/backend/chat.telegram.html),
|
||||
a [Bluetooth
|
||||
scanner](https://docs.platypush.tech/en/latest/platypush/backend/bluetooth.scanner.ble.html)
|
||||
etc.).
|
||||
|
||||
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka,
|
||||
Websocket and TCP) then you can send requests to these services in JSON format.
|
||||
For example, in the case of the HTTP backend:
|
||||
|
||||
```shell
|
||||
# Get a token
|
||||
# Get a token
|
||||
curl -XPOST -H 'Content-Type: application/json' -d '
|
||||
{
|
||||
"username": "$YOUR_USER",
|
||||
"password": "$YOUR_PASSWORD"
|
||||
}' 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",
|
||||
"action": "tts.say",
|
||||
|
@ -154,33 +236,38 @@ curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_
|
|||
}' 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
|
||||
[Bluetooth device is connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
|
||||
or a
|
||||
[Flic button is pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
|
||||
or some
|
||||
[speech is detected on the voice assistant service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
|
||||
or an
|
||||
[RSS feed has new items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
|
||||
or a
|
||||
[new email is received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
|
||||
or a
|
||||
[new track is played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
|
||||
or an
|
||||
[NFC tag is detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
|
||||
or
|
||||
[new sensor data is available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
|
||||
or
|
||||
[a value of a Zigbee device changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
|
||||
etc.), the associated backend will trigger an [event](https://docs.platypush.tech/en/latest/events.html).
|
||||
[Full list](https://docs.platypush.tech/en/latest/events.html)
|
||||
|
||||
When a certain event occurs (e.g. a JSON request is received, or a [Bluetooth
|
||||
device is
|
||||
connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
|
||||
or a [Flic button is
|
||||
pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
|
||||
or some [speech is detected on the voice assistant
|
||||
service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
|
||||
or an [RSS feed has new
|
||||
items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
|
||||
or a [new email is
|
||||
received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
|
||||
or a [new track is
|
||||
played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
|
||||
or an [NFC tag is
|
||||
detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
|
||||
or [new sensor data is
|
||||
available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
|
||||
or [a value of a Zigbee device
|
||||
changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
|
||||
etc.), the associated backend will trigger an
|
||||
[event](https://docs.platypush.tech/en/latest/events.html).
|
||||
|
||||
### Hooks
|
||||
|
||||
Event hooks are custom pieces of logic that will be run when a certain event is triggered. Hooks are the glue that
|
||||
connects events to actions, exposing a paradigm similar to IFTTT (_if a certain event happens then run these actions_).
|
||||
They can declared as:
|
||||
Event hooks are custom pieces of logic that will be run when a certain event is
|
||||
triggered. Hooks are the glue that connects events to actions, exposing a
|
||||
paradigm similar to IFTTT (_if a certain event happens then run these
|
||||
actions_). They can declared as:
|
||||
|
||||
- Sections of the [`config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml).
|
||||
Example:
|
||||
|
@ -204,9 +291,10 @@ event.hook.SearchSongVoiceCommand:
|
|||
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):
|
||||
|
||||
|
||||
```python
|
||||
from platypush.event.hook import hook
|
||||
from platypush.utils import run
|
||||
|
@ -225,13 +313,17 @@ def on_music_play_command(event, title=None, artist=None, **context):
|
|||
|
||||
### Procedures
|
||||
|
||||
Procedures are pieces of custom logic that can be executed as atomic actions using `procedure.<name>` as an action name.
|
||||
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` -
|
||||
provided that the procedure is also imported in `~/.config/platypush/scripts/__init__.py` so it can be discovered by
|
||||
the service.
|
||||
Procedures are pieces of custom logic that can be executed as atomic actions
|
||||
using `procedure.<name>` as an action name.
|
||||
|
||||
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:
|
||||
They can be defined either in the `config.yaml` or as Python scripts stored
|
||||
under `~/.config/platypush/scripts` - provided that the procedure is also
|
||||
imported in `~/.config/platypush/scripts/__init__.py` so it can be discovered
|
||||
by the service.
|
||||
|
||||
YAML example for a procedure that can be executed when we arrive home and turns
|
||||
on the lights if the luminosity is lower that a certain thresholds, says a
|
||||
welcome home message using the TTS engine and starts playing the music:
|
||||
|
||||
```yaml
|
||||
procedure.at_home:
|
||||
|
@ -254,7 +346,7 @@ procedure.at_home:
|
|||
Python example:
|
||||
|
||||
```python
|
||||
# Content of ~/.config/platypush/scripts/home.py
|
||||
# Content of ~/.config/platypush/scripts/home.py
|
||||
from platypush.procedure import procedure
|
||||
from platypush.utils import run
|
||||
|
||||
|
@ -268,11 +360,12 @@ def at_home(**context):
|
|||
run('music.mpd.play')
|
||||
```
|
||||
|
||||
In either case, you can easily trigger the at-home procedure by sending an action request message to a backend - for
|
||||
example, over the HTTP backend:
|
||||
In either case, you can easily trigger the at-home procedure by sending an
|
||||
action request message to a backend - for example, over the HTTP backend:
|
||||
|
||||
```shell
|
||||
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||
curl -XPOST -H 'Content-Type: application/json' \
|
||||
-H "Authorization: Bearer $YOUR_TOKEN" -d '
|
||||
{
|
||||
"type": "request",
|
||||
"action": "procedure.at_home"
|
||||
|
@ -281,15 +374,18 @@ curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_
|
|||
|
||||
### Cronjobs
|
||||
|
||||
Cronjobs are pieces of logic that will be run at regular intervals, expressed in crontab-compatible syntax.
|
||||
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` as
|
||||
functions labelled by the `@cron` decorator.
|
||||
Cronjobs are pieces of logic that will be run at regular intervals, expressed
|
||||
in crontab-compatible syntax. They can be defined either in the `config.yaml`
|
||||
or as Python scripts stored under `~/.config/platypush/scripts` as functions
|
||||
labelled by the `@cron` decorator.
|
||||
|
||||
Note that seconds are also supported (unlike the standard crontab definition), but, for back-compatibility with the
|
||||
standard crontab format, they are at the end of the cron expression, so the expression is actually in the format
|
||||
Note that seconds are also supported (unlike the standard crontab definition),
|
||||
but, for back-compatibility with the standard crontab format, they are at the
|
||||
end of the cron expression, so the expression is actually in the format
|
||||
`<minute> <hour> <day_of_month> <month> <day_of_week> <second>`.
|
||||
|
||||
YAML example for a cronjob that is executed every 30 seconds and checks if a Bluetooth device is nearby:
|
||||
YAML example for a cronjob that is executed every 30 seconds and checks if a
|
||||
Bluetooth device is nearby:
|
||||
|
||||
```yaml
|
||||
cron.check_bt_device:
|
||||
|
@ -308,7 +404,7 @@ cron.check_bt_device:
|
|||
Python example:
|
||||
|
||||
```python
|
||||
# Content of ~/.config/platypush/scripts/bt_cron.py
|
||||
# Content of ~/.config/platypush/scripts/bt_cron.py
|
||||
from platypush.cron import cron
|
||||
from platypush.utils import run
|
||||
|
||||
|
@ -323,15 +419,18 @@ def check_bt_device(**context):
|
|||
|
||||
### The web interface
|
||||
|
||||
If [`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html) is enabled then a web interface
|
||||
will be provided by default on `http://host:8008/`. Besides using the `/execute` endpoint for running requests, the
|
||||
built-in web server also provides a full-featured interface that groups together the controls for most of the plugins -
|
||||
e.g. sensors, switches, music controls and search, media library and torrent management, lights, Zigbee/Z-Wave devices
|
||||
and so on. The UI is responsive and mobile-friendly.
|
||||
If
|
||||
[`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html)
|
||||
is enabled then a web interface will be provided by default on
|
||||
`http://host:8008/`. Besides using the `/execute` endpoint for running
|
||||
requests, the built-in web server also provides a full-featured interface that
|
||||
groups together the controls for most of the plugins - e.g. sensors, switches,
|
||||
music controls and search, media library and torrent management, lights,
|
||||
Zigbee/Z-Wave devices and so on. The UI is responsive and mobile-friendly.
|
||||
|
||||
The web service also provides means for the user to create
|
||||
[custom dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml) that can
|
||||
be used to show information from multiple sources on a large screen.
|
||||
The web service also provides means for the user to create [custom
|
||||
dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml)
|
||||
that can be used to show information from multiple sources on a large screen.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -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:
|
||||
|
||||
```yaml
|
||||
# Example for Debian-based distributions
|
||||
# Example for Debian-based distributions
|
||||
[sudo] apt-get install redis-server
|
||||
|
||||
# Enable and start the service
|
||||
# Enable and start the service
|
||||
[sudo] systemctl enable redis
|
||||
[sudo] systemctl start redis
|
||||
```
|
||||
|
||||
To install the core platform:
|
||||
|
||||
* The `pip` way:
|
||||
#### Install through `pip`
|
||||
|
||||
```shell
|
||||
[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
|
||||
git clone https://git.platypush.tech/platypush/platypush.git
|
||||
cd platypush
|
||||
[sudo] pip install .
|
||||
# Or
|
||||
[sudo] python3 setup.py install
|
||||
```
|
||||
|
||||
Then install the extensions that you wish to use. There are a few ways to check the dependencies required by an
|
||||
extension:
|
||||
### Installing the dependencies for your extensions
|
||||
|
||||
#### 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:
|
||||
|
||||
|
@ -383,15 +515,18 @@ If you follow this route then you can install the extra dependencies in one of t
|
|||
cd $DIR_TO_PLATYPUSH
|
||||
[sudo] pip3 install '.[extra1,extra2,extra3]'
|
||||
```
|
||||
|
||||
#### Check the dependencies/installation instructions reported under the plugin/backend documentation.
|
||||
|
||||
If you follow this route then simply run the commands listed in the plugin/backend documentation to get the dependencies
|
||||
installed.
|
||||
#### Check the instructions reported in the documentation
|
||||
|
||||
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.
|
||||
If you follow this route then simply run the commands listed in the
|
||||
[plugin/backend documentation](https://docs.platypush.tech) to get the
|
||||
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:
|
||||
|
||||
|
@ -400,89 +535,100 @@ platypush
|
|||
```
|
||||
|
||||
It's advised to run it as a systemd service though - simply copy the provided
|
||||
[`.service` file](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/systemd/platypush.service) to
|
||||
`~/.config/systemd/user`, check if the path of `platypush` matches the path where it's installed on your system, and
|
||||
start the service via `systemctl`:
|
||||
[`.service`
|
||||
file](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/systemd/platypush.service)
|
||||
to `~/.config/systemd/user`, check if the path of `platypush` matches the path
|
||||
where it's installed on your system, and start the service via `systemctl`:
|
||||
|
||||
```shell
|
||||
systemctl --user start platypush
|
||||
```
|
||||
|
||||
### [Virtual environment installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-virtual-environment)
|
||||
### Virtual environment installation
|
||||
|
||||
Platypush provides a script named `platyvenv` that can parse a `config.yaml` and automatically create a virtual
|
||||
environment (under `~/.local/share/platypush/venv/<device_id>`) with all the dependencies required by the configured
|
||||
integrations.
|
||||
Platypush provides a script named `platyvenv` that can parse a `config.yaml`
|
||||
and automatically create a virtual environment (under
|
||||
`~/.local/share/platypush/venv/<device_id>`) with all the dependencies required
|
||||
by the configured integrations.
|
||||
|
||||
1. Create the environment from a configuration file:
|
||||
|
||||
```shell
|
||||
platyvenv build -c /path/to/config.yaml
|
||||
```
|
||||
```shell
|
||||
platyvenv build -c /path/to/config.yaml
|
||||
```
|
||||
|
||||
2. Start the service from the virtual environment:
|
||||
|
||||
```shell
|
||||
# device_id matches either the hostname or the device_id in config.yaml
|
||||
platyvenv start device_id
|
||||
```
|
||||
```shell
|
||||
# device_id matches either the hostname or the device_id in config.yaml
|
||||
platyvenv start device_id
|
||||
```
|
||||
|
||||
3. Stop the instance:
|
||||
|
||||
```shell
|
||||
platyvenv stop device_id
|
||||
```
|
||||
```shell
|
||||
platyvenv stop device_id
|
||||
```
|
||||
|
||||
4. Remove the instance:
|
||||
|
||||
```shell
|
||||
platyvenv rm device_id
|
||||
```
|
||||
```shell
|
||||
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
|
||||
creates a container instance from a `config.yaml`:
|
||||
### Docker installation
|
||||
|
||||
You can also install Platypush in a container - the application provides a
|
||||
script named `platydock` that automatically creates a container instance from a
|
||||
`config.yaml`:
|
||||
|
||||
1. Create the container from a configuration file:
|
||||
|
||||
```shell
|
||||
platydock build -c /path/to/config.yaml
|
||||
```
|
||||
```shell
|
||||
platydock build -c /path/to/config.yaml
|
||||
```
|
||||
|
||||
2. Start the container:
|
||||
|
||||
```shell
|
||||
# device_id matches either the hostname or the device_id in config.yaml
|
||||
platydock start device_id
|
||||
```
|
||||
```shell
|
||||
# device_id matches either the hostname or the device_id in config.yaml
|
||||
platydock start device_id
|
||||
```
|
||||
|
||||
3. Stop the instance:
|
||||
|
||||
```shell
|
||||
platydock stop device_id
|
||||
```
|
||||
```shell
|
||||
platydock stop device_id
|
||||
```
|
||||
|
||||
4. Remove the instance:
|
||||
|
||||
```shell
|
||||
platydock rm device_id
|
||||
```
|
||||
```shell
|
||||
platydock rm device_id
|
||||
```
|
||||
|
||||
Note that both the virtual environment and Docker container option offer the possibility to include extra YAML configuration
|
||||
files in the main `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`.
|
||||
Note that both the virtual environment and Docker container option offer the
|
||||
possibility to include extra YAML configuration files in the main `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
|
||||
|
||||
An [official Android app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on the F-Droid store.
|
||||
It allows to easily discover and manage multiple Platypush services on a network through the web interface, and it
|
||||
easily brings the power of Platypush to your fingertips.
|
||||
An [official Android
|
||||
app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on
|
||||
the F-Droid store. It allows to easily discover and manage multiple Platypush
|
||||
services on a network through the web interface, and it easily brings the power
|
||||
of Platypush to your fingertips.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
|
||||
Or run the following command from the project root folder:
|
||||
To run the tests simply run `pytest` either from the project root folder or the
|
||||
`tests/` folder. Or run the following command from the project root folder:
|
||||
|
||||
```shell
|
||||
python -m tests
|
||||
|
@ -494,10 +640,11 @@ python -m tests
|
|||
|
||||
If you use and love Platypush, please consider [buying me a coffee/beer](https://paypal.me/fabiomanganiello).
|
||||
|
||||
I've been working on Platypush all by myself in my spare time for the past few years, and I've made sure that it remains
|
||||
open and free.
|
||||
I've been working on Platypush all by myself in my spare time for the past few
|
||||
years, and I've made sure that it remains open and free.
|
||||
|
||||
If you like this product, please consider supporting - I'm definitely not planning to get rich with this project, but
|
||||
I'd love to have at least the costs for the server covered by users.
|
||||
If you like this product, please consider supporting - I'm definitely not
|
||||
planning to get rich with this project, but I'd love to have at least the costs
|
||||
for the server covered by users.
|
||||
|
||||
Issues and requests opened by donors will also be given priority over others.
|
||||
|
|
|
@ -3,10 +3,12 @@ import json
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from random import randint
|
||||
from typing import Union, List
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import Directive
|
||||
from marshmallow import fields
|
||||
|
||||
|
||||
class SchemaDirective(Directive):
|
||||
|
@ -22,10 +24,36 @@ class SchemaDirective(Directive):
|
|||
|
||||
sys.path.insert(0, _schemas_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_field_value(field) -> str:
|
||||
@classmethod
|
||||
def _get_field_value(cls, field):
|
||||
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]]:
|
||||
m = self._schema_regex.match('\n'.join(self.content))
|
||||
|
|
|
@ -17,9 +17,7 @@ Backends
|
|||
platypush/backend/button.flic.rst
|
||||
platypush/backend/camera.pi.rst
|
||||
platypush/backend/chat.telegram.rst
|
||||
platypush/backend/clipboard.rst
|
||||
platypush/backend/covid19.rst
|
||||
platypush/backend/dbus.rst
|
||||
platypush/backend/file.monitor.rst
|
||||
platypush/backend/foursquare.rst
|
||||
platypush/backend/github.rst
|
||||
|
|
|
@ -195,6 +195,10 @@ intersphinx_mapping = {'https://docs.python.org/': None}
|
|||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
autodoc_default_options = {
|
||||
'inherited-members': True,
|
||||
}
|
||||
|
||||
autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||
'google.assistant.embedded',
|
||||
'google.assistant.library',
|
||||
|
@ -212,7 +216,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'gevent.wsgi',
|
||||
'Adafruit_IO',
|
||||
'pyperclip',
|
||||
'dbus',
|
||||
'pydbus',
|
||||
'inputs',
|
||||
'inotify',
|
||||
'omxplayer',
|
||||
|
@ -272,7 +276,6 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'gi',
|
||||
'gi.repository',
|
||||
'twilio',
|
||||
'pytz',
|
||||
'Adafruit_Python_DHT',
|
||||
'RPi.GPIO',
|
||||
'RPLCD',
|
||||
|
@ -281,6 +284,13 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'aiohttp',
|
||||
'watchdog',
|
||||
'pyngrok',
|
||||
'irc',
|
||||
'irc.bot',
|
||||
'irc.strings',
|
||||
'irc.client',
|
||||
'irc.connection',
|
||||
'irc.events',
|
||||
'defusedxml',
|
||||
]
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
|
|
@ -18,6 +18,7 @@ Events
|
|||
platypush/events/clipboard.rst
|
||||
platypush/events/covid19.rst
|
||||
platypush/events/custom.rst
|
||||
platypush/events/dbus.rst
|
||||
platypush/events/distance.rst
|
||||
platypush/events/file.rst
|
||||
platypush/events/foursquare.rst
|
||||
|
@ -27,11 +28,13 @@ Events
|
|||
platypush/events/google.fit.rst
|
||||
platypush/events/google.pubsub.rst
|
||||
platypush/events/gotify.rst
|
||||
platypush/events/gpio.rst
|
||||
platypush/events/gps.rst
|
||||
platypush/events/http.rst
|
||||
platypush/events/http.hook.rst
|
||||
platypush/events/http.rss.rst
|
||||
platypush/events/inotify.rst
|
||||
platypush/events/irc.rst
|
||||
platypush/events/joystick.rst
|
||||
platypush/events/kafka.rst
|
||||
platypush/events/light.rst
|
||||
|
@ -46,9 +49,11 @@ Events
|
|||
platypush/events/nextcloud.rst
|
||||
platypush/events/nfc.rst
|
||||
platypush/events/ngrok.rst
|
||||
platypush/events/ntfy.rst
|
||||
platypush/events/ping.rst
|
||||
platypush/events/pushbullet.rst
|
||||
platypush/events/qrcode.rst
|
||||
platypush/events/rss.rst
|
||||
platypush/events/scard.rst
|
||||
platypush/events/sensor.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:
|
||||
|
||||
* 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 `Blog articles`_ for inspiration on use-cases possible projects
|
||||
|
||||
.. _main page: https://platypush.tech
|
||||
.. _Gitlab page: https://git.platypush.tech/platypush/platypush
|
||||
.. _online wiki: https://git.platypush.tech/platypush/platypush/-/wikis/home
|
||||
.. _Gitea page: https://git.platypush.tech/platypush/platypush
|
||||
.. _online wiki: https://git.platypush.tech/platypush/platypush/wiki
|
||||
.. _Blog articles: https://blog.platypush.tech
|
||||
|
||||
.. 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
|
||||
:members:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
``dbus``
|
||||
========
|
||||
|
||||
.. automodule:: platypush.message.event.dbus
|
||||
:members:
|
|
@ -1,5 +1,5 @@
|
|||
``platypush.message.event.gotify``
|
||||
==================================
|
||||
``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
|
||||
: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
|
||||
:members:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
``chat.irc``
|
||||
============
|
||||
|
||||
.. automodule:: platypush.plugins.chat.irc
|
||||
: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.ir.mlx90640.rst
|
||||
platypush/plugins/camera.pi.rst
|
||||
platypush/plugins/chat.irc.rst
|
||||
platypush/plugins/chat.telegram.rst
|
||||
platypush/plugins/clipboard.rst
|
||||
platypush/plugins/config.rst
|
||||
|
@ -72,8 +73,11 @@ Plugins
|
|||
platypush/plugins/luma.oled.rst
|
||||
platypush/plugins/mail.imap.rst
|
||||
platypush/plugins/mail.smtp.rst
|
||||
platypush/plugins/mailgun.rst
|
||||
platypush/plugins/mastodon.rst
|
||||
platypush/plugins/media.chromecast.rst
|
||||
platypush/plugins/media.gstreamer.rst
|
||||
platypush/plugins/media.jellyfin.rst
|
||||
platypush/plugins/media.kodi.rst
|
||||
platypush/plugins/media.mplayer.rst
|
||||
platypush/plugins/media.mpv.rst
|
||||
|
@ -92,6 +96,7 @@ Plugins
|
|||
platypush/plugins/nextcloud.rst
|
||||
platypush/plugins/ngrok.rst
|
||||
platypush/plugins/nmap.rst
|
||||
platypush/plugins/ntfy.rst
|
||||
platypush/plugins/otp.rst
|
||||
platypush/plugins/pihole.rst
|
||||
platypush/plugins/ping.rst
|
||||
|
@ -100,6 +105,7 @@ Plugins
|
|||
platypush/plugins/pwm.pca9685.rst
|
||||
platypush/plugins/qrcode.rst
|
||||
platypush/plugins/redis.rst
|
||||
platypush/plugins/rss.rst
|
||||
platypush/plugins/rtorrent.rst
|
||||
platypush/plugins/serial.rst
|
||||
platypush/plugins/shell.rst
|
||||
|
|
|
@ -6,11 +6,17 @@ from platypush.plugins import Plugin
|
|||
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():
|
||||
manifests = {mf.component_name for mf in get_manifests(Plugin)}
|
||||
return {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -19,17 +25,17 @@ def get_all_backends():
|
|||
manifests = {mf.component_name for mf in get_manifests(Backend)}
|
||||
return {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
def get_all_events():
|
||||
return get_plugin('inspect').get_all_events().output
|
||||
return _get_inspect_plugin().get_all_events().output
|
||||
|
||||
|
||||
def get_all_responses():
|
||||
return get_plugin('inspect').get_all_responses().output
|
||||
return _get_inspect_plugin().get_all_responses().output
|
||||
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
|
@ -100,16 +106,17 @@ Backends
|
|||
|
||||
# noinspection DuplicatedCode
|
||||
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_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:
|
||||
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):
|
||||
header = '``{}``'.format(event)
|
||||
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])
|
||||
|
||||
with open(event_file, 'w') as f:
|
||||
|
@ -127,21 +134,22 @@ 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
|
||||
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_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:
|
||||
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):
|
||||
header = '``{}``'.format(response)
|
||||
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])
|
||||
|
||||
with open(response_file, 'w') as f:
|
||||
|
@ -159,7 +167,7 @@ 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()
|
||||
|
|
|
@ -23,13 +23,13 @@ from .message.response import Response
|
|||
from .utils import set_thread_name, get_enabled_plugins
|
||||
|
||||
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
||||
__version__ = '0.22.3'
|
||||
__version__ = '0.23.3'
|
||||
|
||||
logger = logging.getLogger('platypush')
|
||||
|
||||
|
||||
class Daemon:
|
||||
""" Main class for the Platypush daemon """
|
||||
"""Main class for the Platypush daemon"""
|
||||
|
||||
# Configuration file (default: either ~/.config/platypush/config.yaml or
|
||||
# /etc/platypush/config.yaml
|
||||
|
@ -51,8 +51,15 @@ class Daemon:
|
|||
# number of executions retries before a request fails
|
||||
n_tries = 2
|
||||
|
||||
def __init__(self, config_file=None, pidfile=None, requests_to_process=None,
|
||||
no_capture_stdout=False, no_capture_stderr=False, redis_queue=None):
|
||||
def __init__(
|
||||
self,
|
||||
config_file=None,
|
||||
pidfile=None,
|
||||
requests_to_process=None,
|
||||
no_capture_stdout=False,
|
||||
no_capture_stderr=False,
|
||||
redis_queue=None,
|
||||
):
|
||||
"""
|
||||
Constructor
|
||||
Params:
|
||||
|
@ -80,8 +87,11 @@ class Daemon:
|
|||
logging.basicConfig(**Config.get('logging'))
|
||||
|
||||
redis_conf = Config.get('backend.redis') or {}
|
||||
self.bus = RedisBus(redis_queue=self.redis_queue, on_message=self.on_message(),
|
||||
**redis_conf.get('redis_args', {}))
|
||||
self.bus = RedisBus(
|
||||
redis_queue=self.redis_queue,
|
||||
on_message=self.on_message(),
|
||||
**redis_conf.get('redis_args', {})
|
||||
)
|
||||
|
||||
self.no_capture_stdout = no_capture_stdout
|
||||
self.no_capture_stderr = no_capture_stderr
|
||||
|
@ -98,33 +108,59 @@ class Daemon:
|
|||
args -- Your sys.argv[1:] [List of strings]
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--config', '-c', dest='config', required=False,
|
||||
default=None, help=cls.config_file.__doc__)
|
||||
parser.add_argument('--pidfile', '-P', dest='pidfile', required=False,
|
||||
default=None, help="File where platypush will " +
|
||||
"store its PID, useful if you're planning to " +
|
||||
"integrate it in a service")
|
||||
parser.add_argument('--no-capture-stdout', 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, action='store_true',
|
||||
default=cls._default_redis_queue,
|
||||
help="Name of the Redis queue to be used to internally deliver messages "
|
||||
"(default: platypush/bus)")
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
'-c',
|
||||
dest='config',
|
||||
required=False,
|
||||
default=None,
|
||||
help=cls.config_file.__doc__,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pidfile',
|
||||
'-P',
|
||||
dest='pidfile',
|
||||
required=False,
|
||||
default=None,
|
||||
help="File where platypush will "
|
||||
+ "store its PID, useful if you're planning to "
|
||||
+ "integrate it in a service",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-capture-stdout',
|
||||
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)
|
||||
return cls(config_file=opts.config, pidfile=opts.pidfile,
|
||||
no_capture_stdout=opts.no_capture_stdout,
|
||||
no_capture_stderr=opts.no_capture_stderr,
|
||||
redis_queue=opts.redis_queue)
|
||||
return cls(
|
||||
config_file=opts.config,
|
||||
pidfile=opts.pidfile,
|
||||
no_capture_stdout=opts.no_capture_stdout,
|
||||
no_capture_stderr=opts.no_capture_stderr,
|
||||
redis_queue=opts.redis_queue,
|
||||
)
|
||||
|
||||
def on_message(self):
|
||||
"""
|
||||
|
@ -145,8 +181,10 @@ class Daemon:
|
|||
logger.info('Dropped unauthorized request: {}'.format(msg))
|
||||
|
||||
self.processed_requests += 1
|
||||
if self.requests_to_process \
|
||||
and self.processed_requests >= self.requests_to_process:
|
||||
if (
|
||||
self.requests_to_process
|
||||
and self.processed_requests >= self.requests_to_process
|
||||
):
|
||||
self.stop_app()
|
||||
elif isinstance(msg, Response):
|
||||
logger.info('Received response: {}'.format(msg))
|
||||
|
@ -158,7 +196,7 @@ class Daemon:
|
|||
return _f
|
||||
|
||||
def stop_app(self):
|
||||
""" Stops the backends and the bus """
|
||||
"""Stops the backends and the bus"""
|
||||
from .plugins import RunnablePlugin
|
||||
|
||||
for backend in self.backends.values():
|
||||
|
@ -173,7 +211,7 @@ class Daemon:
|
|||
self.cron_scheduler.stop()
|
||||
|
||||
def run(self):
|
||||
""" Start the daemon """
|
||||
"""Start the daemon"""
|
||||
if not self.no_capture_stdout:
|
||||
sys.stdout = Logger(logger.info)
|
||||
if not self.no_capture_stderr:
|
||||
|
|
|
@ -99,6 +99,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
|||
if self._is_expected_response(msg):
|
||||
# Expected response, trigger the response handler
|
||||
clear_timeout()
|
||||
# pylint: disable=unsubscriptable-object
|
||||
self._request_context['on_response'](msg)
|
||||
self.stop()
|
||||
return
|
||||
|
@ -110,12 +111,13 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
|||
""" Internal only - returns true if we are expecting for a response
|
||||
and msg is that response """
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
return self._request_context \
|
||||
and isinstance(msg, Response) \
|
||||
and msg.id == self._request_context['request'].id
|
||||
|
||||
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)
|
||||
|
||||
def _setup_response_handler(self, request, on_response, response_timeout):
|
||||
|
@ -196,7 +198,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
|||
|
||||
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.
|
||||
To be implemented in the derived classes. By default, if the Redis
|
||||
|
@ -213,8 +215,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
|||
if not redis:
|
||||
raise KeyError()
|
||||
except KeyError:
|
||||
self.logger.warning("Backend {} does not implement send_message " +
|
||||
"and the fallback Redis backend isn't configured")
|
||||
self.logger.warning((
|
||||
"Backend {} does not implement send_message "
|
||||
"and the fallback Redis backend isn't configured"
|
||||
).format(self.__class__.__name__))
|
||||
return
|
||||
|
||||
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:
|
||||
try:
|
||||
# pylint: disable=not-callable
|
||||
self.loop()
|
||||
except Exception as e:
|
||||
has_error = True
|
||||
|
@ -259,7 +264,6 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
|||
|
||||
def on_stop(self):
|
||||
""" Callback invoked when the process stops """
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
""" 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')
|
||||
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')
|
||||
redis_args = {}
|
||||
else:
|
||||
|
|
|
@ -84,7 +84,9 @@ class BluetoothScannerBackend(SensorBackend):
|
|||
with self._bt_lock:
|
||||
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():
|
||||
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):
|
||||
resource = MonitoredResource(resource)
|
||||
elif isinstance(resource, dict):
|
||||
if 'patterns' in resource or 'ignore_patterns' in resource:
|
||||
resource = MonitoredPattern(**resource)
|
||||
elif 'regexes' in resource or 'ignore_regexes' in resource:
|
||||
if 'regexes' in resource or 'ignore_regexes' in resource:
|
||||
resource = MonitoredRegex(**resource)
|
||||
elif 'patterns' in resource or 'ignore_patterns' in resource or 'ignore_directories' in resource:
|
||||
resource = MonitoredPattern(**resource)
|
||||
else:
|
||||
resource = MonitoredResource(**resource)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler, RegexMatchingEventHandler
|
||||
|
||||
|
@ -16,14 +17,50 @@ class EventHandler(FileSystemEventHandler):
|
|||
resource.path = os.path.expanduser(resource.path)
|
||||
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):
|
||||
get_bus().post(FileSystemCreateEvent(path=event.src_path, is_directory=event.is_directory))
|
||||
self._on_event(event, FileSystemCreateEvent)
|
||||
|
||||
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):
|
||||
get_bus().post(FileSystemModifyEvent(path=event.src_path, is_directory=event.is_directory))
|
||||
self._on_event(event, FileSystemModifyEvent)
|
||||
|
||||
def on_moved(self, event):
|
||||
pass
|
||||
|
|
|
@ -4,7 +4,6 @@ import threading
|
|||
|
||||
from typing import Optional, List
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from sqlalchemy import create_engine, Column, String, DateTime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
@ -129,7 +128,7 @@ class GithubBackend(Backend):
|
|||
def _get_last_event_time(self, uri: str):
|
||||
with self.db_lock:
|
||||
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):
|
||||
with self.db_lock:
|
||||
|
|
|
@ -91,14 +91,16 @@ class HttpBackend(Backend):
|
|||
other music plugin enabled. -->
|
||||
<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" />
|
||||
</Row>
|
||||
|
||||
<!-- Display the following widgets on a second row -->
|
||||
<Row>
|
||||
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
||||
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
|
||||
explicitly exposed as an HTTP resource through the backend
|
||||
`resource_dirs` attribute. -->
|
||||
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
||||
|
||||
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
||||
|
@ -151,34 +153,41 @@ class HttpBackend(Backend):
|
|||
|
||||
Requires:
|
||||
|
||||
* **flask** (``pip install flask``)
|
||||
* **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.
|
||||
* **gunicorn** (``pip install gunicorn``) - optional, to run the Platypush webapp over uWSGI.
|
||||
|
||||
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_WEBSOCKET_PORT = 8009
|
||||
|
||||
def __init__(self, port=_DEFAULT_HTTP_PORT,
|
||||
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
||||
bind_address='0.0.0.0',
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
port=_DEFAULT_HTTP_PORT,
|
||||
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
||||
bind_address='0.0.0.0',
|
||||
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)
|
||||
:type port: int
|
||||
|
@ -245,26 +254,37 @@ class HttpBackend(Backend):
|
|||
self.bind_address = bind_address
|
||||
|
||||
if resource_dirs:
|
||||
self.resource_dirs = {name: os.path.abspath(
|
||||
os.path.expanduser(d)) for name, d in resource_dirs.items()}
|
||||
self.resource_dirs = {
|
||||
name: os.path.abspath(os.path.expanduser(d))
|
||||
for name, d in resource_dirs.items()
|
||||
}
|
||||
else:
|
||||
self.resource_dirs = {}
|
||||
|
||||
self.active_websockets = set()
|
||||
self.run_externally = run_externally
|
||||
self.uwsgi_args = uwsgi_args or []
|
||||
self.ssl_context = get_ssl_server_context(ssl_cert=ssl_cert,
|
||||
ssl_key=ssl_key,
|
||||
ssl_cafile=ssl_cafile,
|
||||
ssl_capath=ssl_capath) \
|
||||
if ssl_cert else None
|
||||
self.ssl_context = (
|
||||
get_ssl_server_context(
|
||||
ssl_cert=ssl_cert,
|
||||
ssl_key=ssl_key,
|
||||
ssl_cafile=ssl_cafile,
|
||||
ssl_capath=ssl_capath,
|
||||
)
|
||||
if ssl_cert
|
||||
else None
|
||||
)
|
||||
|
||||
if self.uwsgi_args:
|
||||
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \
|
||||
['--module', 'platypush.backend.http.uwsgi', '--enable-threads']
|
||||
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [
|
||||
'--module',
|
||||
'platypush.backend.http.uwsgi',
|
||||
'--enable-threads',
|
||||
]
|
||||
|
||||
self.local_base_url = '{proto}://localhost:{port}'.\
|
||||
format(proto=('https' if ssl_cert else 'http'), port=self.port)
|
||||
self.local_base_url = '{proto}://localhost:{port}'.format(
|
||||
proto=('https' if ssl_cert else 'http'), port=self.port
|
||||
)
|
||||
|
||||
self._websocket_lock_timeout = 10
|
||||
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')
|
||||
|
||||
def on_stop(self):
|
||||
""" On backend stop """
|
||||
"""On backend stop"""
|
||||
super().on_stop()
|
||||
self.logger.info('Received STOP event on HttpBackend')
|
||||
|
||||
|
@ -283,7 +303,9 @@ class HttpBackend(Backend):
|
|||
self.server_proc.kill()
|
||||
self.server_proc.wait(timeout=10)
|
||||
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:
|
||||
self.logger.info('HTTP server process terminated')
|
||||
else:
|
||||
|
@ -292,17 +314,25 @@ class HttpBackend(Backend):
|
|||
if self.server_proc.is_alive():
|
||||
self.server_proc.kill()
|
||||
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:
|
||||
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.logger.info('HTTP websocket service terminated')
|
||||
|
||||
def _acquire_websocket_lock(self, ws):
|
||||
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:
|
||||
raise TimeoutError('Websocket lock acquire timeout')
|
||||
|
||||
|
@ -312,13 +342,19 @@ class HttpBackend(Backend):
|
|||
finally:
|
||||
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:
|
||||
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):
|
||||
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:
|
||||
raise TimeoutError('Websocket lock acquire timeout')
|
||||
|
||||
|
@ -326,12 +362,15 @@ class HttpBackend(Backend):
|
|||
if addr in self._websocket_locks:
|
||||
self._websocket_locks[addr].release()
|
||||
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:
|
||||
self._websocket_lock.release()
|
||||
|
||||
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):
|
||||
try:
|
||||
self._acquire_websocket_lock(ws)
|
||||
|
@ -348,26 +387,35 @@ class HttpBackend(Backend):
|
|||
try:
|
||||
loop.run_until_complete(send_event(_ws))
|
||||
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)
|
||||
if _ws.remote_address in self._websocket_locks:
|
||||
del self._websocket_locks[_ws.remote_address]
|
||||
|
||||
def websocket(self):
|
||||
""" Websocket main server """
|
||||
"""Websocket main server"""
|
||||
set_thread_name('WebsocketServer')
|
||||
|
||||
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>'
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
await websocket.recv()
|
||||
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)
|
||||
if address in self._websocket_locks:
|
||||
del self._websocket_locks[address]
|
||||
|
@ -378,8 +426,13 @@ class HttpBackend(Backend):
|
|||
|
||||
self._websocket_loop = get_or_create_event_loop()
|
||||
self._websocket_loop.run_until_complete(
|
||||
websocket_serve(register_websocket, self.bind_address, self.websocket_port,
|
||||
**websocket_args))
|
||||
websocket_serve(
|
||||
register_websocket,
|
||||
self.bind_address,
|
||||
self.websocket_port,
|
||||
**websocket_args
|
||||
)
|
||||
)
|
||||
self._websocket_loop.run_forever()
|
||||
|
||||
def _start_web_server(self):
|
||||
|
@ -402,7 +455,11 @@ class HttpBackend(Backend):
|
|||
|
||||
def run(self):
|
||||
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:
|
||||
self.logger.info('Initializing websocket interface')
|
||||
|
@ -410,8 +467,9 @@ class HttpBackend(Backend):
|
|||
self.websocket_thread.start()
|
||||
|
||||
if not self.run_externally:
|
||||
self.server_proc = Process(target=self._start_web_server(),
|
||||
name='WebServer')
|
||||
self.server_proc = Process(
|
||||
target=self._start_web_server(), name='WebServer'
|
||||
)
|
||||
self.server_proc.start()
|
||||
self.server_proc.join()
|
||||
elif self.uwsgi_args:
|
||||
|
@ -419,9 +477,11 @@ class HttpBackend(Backend):
|
|||
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
|
||||
self.server_proc = subprocess.Popen(uwsgi_cmd)
|
||||
else:
|
||||
self.logger.info('The web server is configured to be launched externally but ' +
|
||||
'no uwsgi_args were provided. Make sure that you run another external service' +
|
||||
'for the webserver (e.g. nginx)')
|
||||
self.logger.info(
|
||||
'The web server is configured to be launched externally but '
|
||||
+ '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:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
from flask import Blueprint, request, abort, jsonify
|
||||
|
||||
|
@ -18,7 +17,7 @@ __routes__ = [
|
|||
|
||||
|
||||
@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
|
||||
structure:
|
||||
|
|
|
@ -16,12 +16,14 @@ __routes__ = [
|
|||
@execute.route('/execute', methods=['POST'])
|
||||
@authenticate()
|
||||
def execute():
|
||||
""" Endpoint to execute commands """
|
||||
"""Endpoint to execute commands"""
|
||||
try:
|
||||
msg = json.loads(request.data.decode('utf-8'))
|
||||
except Exception as e:
|
||||
logger().error('Unable to parse JSON from request {}: {}'.format(request.data, str(e)))
|
||||
return abort(400, str(e))
|
||||
logger().error(
|
||||
'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))
|
||||
|
||||
|
@ -29,8 +31,10 @@ def execute():
|
|||
response = send_message(msg)
|
||||
return Response(str(response or {}), mimetype='application/json')
|
||||
except Exception as e:
|
||||
logger().error('Error while running HTTP action: {}. Request: {}'.format(str(e), msg))
|
||||
return abort(500, str(e))
|
||||
logger().error(
|
||||
'Error while running HTTP action: {}. Request: {}'.format(str(e), msg)
|
||||
)
|
||||
abort(500, str(e))
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -13,17 +13,19 @@ __routes__ = [
|
|||
|
||||
@logout.route('/logout', methods=['GET', 'POST'])
|
||||
def logout():
|
||||
""" Logout page """
|
||||
"""Logout page"""
|
||||
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')
|
||||
|
||||
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:
|
||||
return abort(403, 'Invalid session token')
|
||||
abort(403, 'Invalid session token')
|
||||
|
||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
||||
response = make_response(redirect_target)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import json
|
||||
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.utils import authenticate
|
||||
|
@ -17,19 +18,22 @@ __routes__ = [
|
|||
|
||||
|
||||
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:
|
||||
with session.stream.ready:
|
||||
session.stream.ready.wait(timeout=timeout)
|
||||
return session.stream.frame
|
||||
def get_frame(session: Camera, timeout: Optional[float] = None) -> Optional[bytes]:
|
||||
if session.stream:
|
||||
with session.stream.ready:
|
||||
session.stream.ready.wait(timeout=timeout)
|
||||
return session.stream.frame
|
||||
|
||||
|
||||
def feed(plugin: str, **kwargs):
|
||||
plugin = get_camera(plugin)
|
||||
with plugin.open(stream=True, **kwargs) as session:
|
||||
plugin.start_camera(session)
|
||||
def feed(camera: CameraPlugin, **kwargs):
|
||||
with camera.open(**kwargs) as session:
|
||||
camera.start_camera(session)
|
||||
while True:
|
||||
frame = get_frame(session, timeout=5.0)
|
||||
if frame:
|
||||
|
@ -77,8 +81,12 @@ def get_photo(plugin, extension):
|
|||
@authenticate()
|
||||
def get_video(plugin, extension):
|
||||
stream_class = StreamWriter.get_class_by_name(extension)
|
||||
return Response(feed(plugin, stream_format=extension, frames_dir=None, **get_args(request.args)),
|
||||
mimetype=stream_class.mimetype)
|
||||
camera = get_camera(plugin)
|
||||
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'])
|
||||
|
|
|
@ -41,8 +41,11 @@ def resources_path(path):
|
|||
real_base_path = os.path.abspath(os.path.expanduser(resource_dirs[base_path]))
|
||||
real_path = real_base_path
|
||||
|
||||
file_path = [s for s in re.sub(r'^{}(.*)$'.format(base_path), '\\1', path)
|
||||
.split('/') if s]
|
||||
file_path = [
|
||||
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]:
|
||||
real_path += os.sep + p
|
||||
|
|
|
@ -2,8 +2,6 @@ manifest:
|
|||
events: {}
|
||||
install:
|
||||
pip:
|
||||
- flask
|
||||
- bcrypt
|
||||
- python-magic
|
||||
- gunicorn
|
||||
package: platypush.backend.http
|
||||
type: backend
|
||||
|
|
|
@ -7,6 +7,9 @@ from platypush.backend.http.request import HttpRequest
|
|||
|
||||
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
|
||||
the bus whenever something new happened. Supported types:
|
||||
: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
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