Compare commits

..

No commits in common. "master" and "v0.13.1" have entirely different histories.

2130 changed files with 22086 additions and 149522 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [BlackLight]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

31
.github/workflows/python-publish.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

4
.gitignore vendored
View File

@ -3,7 +3,7 @@
*.pyc
__pycache__
build/
/dist/
dist/
*.egg-info/
package.sh
.pypirc
@ -17,5 +17,3 @@ platypush/backend/http/static/js/lib/vue.js
platypush/notebooks
platypush/requests
/http-client.env.json
/platypush/backend/http/static/css/dist
/tests/etc/dashboards

6
.gitmodules vendored
View File

@ -1,3 +1,9 @@
[submodule "docs/wiki"]
path = docs/wiki
url = https://github.com/BlackLight/platypush.wiki.git
[submodule "platypush/plugins/gpio/sensor/ir/mlx90640/lib"]
path = platypush/plugins/camera/ir/mlx90640/lib
url = https://github.com/pimoroni/mlx90640-library
[submodule "webext"]
path = webext
url = https://github.com/BlackLight/platypush-webext.git

7
.readthedocs.yml Normal file
View File

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

9
.travis.requirements Normal file
View File

@ -0,0 +1,9 @@
pyyaml
requests
flask
redis
python-dateutil
websockets
bcrypt
sqlalchemy
croniter

22
.travis.yml Normal file
View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017, 2020 Fabio Manganiello
Copyright (c) 2017, 2018 Fabio Manganiello
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,2 +1,3 @@
recursive-include platypush/backend/http/webapp/dist *
recursive-include platypush/backend/http/static *
recursive-include platypush/backend/http/templates *
include platypush/plugins/http/webpage/mercury-parser.js

553
README.md
View File

@ -1,509 +1,68 @@
Platypush
=========
[![Build Status](https://ci.platypush.tech/status.svg)](https://ci.platypush.tech/latest.log)
[![Documentation Status](https://ci.platypush.tech/docs/status.svg)](https://ci.platypush.tech/docs/latest.log)
[![Build Status](https://travis-ci.org/BlackLight/platypush.svg?branch=master)](https://travis-ci.org/BlackLight/platypush)
[![Documentation Status](https://readthedocs.org/projects/platypush/badge/?version=latest)](https://platypush.readthedocs.io/en/latest/?badge=latest)
[![pip version](https://img.shields.io/pypi/v/platypush.svg?style=flat)](https://pypi.python.org/pypi/platypush/)
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:javascript)
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).
Advised read: [**Getting started with Platypush**](https://medium.com/@automationguru/automate-your-house-your-life-and-everything-else-around-with-platypush-dba1cd13e3f6) (Medium article).
[Reddit channel](https://www.reddit.com/r/platypush)
- The [blog](https://blog.platypush.tech) is in general a good place to get more insights on what you can build with it and inspiration about possible usages.
Imagine Platypush as some kind of [IFTTT](https://ifttt.com) on steroids - or [Tasker](https://tasker.joaoapps.com/), or [Microsoft Flow](https://flow.microsoft.com), or [PushBullet](https://pushbullet.com) on steroids.
Platypush aims to turn any device in a smart hub that can control things, interact with cloud services and send messages to other devices. It's a general-purpose lightweight platform to process any request and run any logic triggered by custom events.
- The [wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home) also contains many resources on getting started.
- Extensive documentation for all the available integrations and messages [is available](https://docs.platypush.tech/).
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
- A [Reddit channel](https://www.reddit.com/r/platypush) is also available for more general questions.
---
Platypush is a general-purpose extensible platform for automation and integration across multiple services and devices.
It enables users to create their own self-hosted pieces of automation based on events (*if this happens then do that*)
and it provides a comprehensive and customizable user interface that collects everything you need to visualize and
control under one roof.
It takes some concepts from [IFTTT](https://ifttt.com), [Tasker](https://tasker.joaoapps.com/),
[Microsoft Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
[Home Assistant](https://www.home-assistant.io/) to provide an environment where the user can easily connect things
together.
Its ideal home is a single-board computer like a RaspberryPi that you can configure to orchestrate any home automation
and cloud automation in your own living room or garage, but it can easily run on any device that can run a Python
interpreter, and the bar for the hardware requirements is very low as well - I use it to run pieces of automation on
devices as powerful as a RaspberryPi Zero or an old Nokia N900 with Linux.
Imagine the ability of running any task you like, or automate any routine you like, on any of your devices. And the flexibility of executing actions through a cloud service, with the power of running them from your laptop, Raspberry Pi, smart home device or smartphone.
You can use Platypush to do things like:
- [Control your smart home lights](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush)
- [Control your music and synchronize it to multiple devices](https://blog.platypush.tech/article/Build-your-open-source-multi-room-and-multi-provider-sound-server-with-Platypush-Mopidy-and-Snapcast)
- [Create custom and privacy-secure voice assistants that run custom hooks on your phrases](https://blog.platypush.tech/article/Build-custom-voice-assistants)
- Build integrations between [sensors](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
[cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html) and
[machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html) to create smart
pieces of automation for e.g.
[people detection](https://blog.platypush.tech/article/Detect-people-with-a-RaspberryPi-a-thermal-camera-Platypush-and-a-pinch-of-machine-learning)
or [sound detection](https://blog.platypush.tech/article/Create-your-smart-baby-monitor-with-Platypush-and-Tensorflow)
- [Get events from your Google or Facebook calendars](https://docs.platypush.tech/en/latest/platypush/plugins/calendar.html)
- [Read data from your sensors and trigger custom events whenever they go above or below some custom thresholds](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
- [Control and automate a self-built robot](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.zeroborg.html)
- [Deliver automated newsletters from custom RSS digests](https://blog.platypush.tech/article/Deliver-customized-newsletters-from-RSS-feeds-with-Platypush)
- [Synchronize the clipboards on your devices](https://docs.platypush.tech/en/latest/platypush/plugins/clipboard.html)
- [Control your smart switches](https://docs.platypush.tech/en/latest/platypush/plugins/switch.html)
- [Implement automated custom text-to-speech routines](https://docs.platypush.tech/en/latest/platypush/plugins/tts.html)
- [Build any kind of interactions and automation routines with your Android device using Tasker](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
- Play [local videos](https://docs.platypush.tech/en/latest/platypush/plugins/media.mpv.html), YouTube videos and torrent media from any device and service, to any device
- [Get weather forecast events for your location and build automation routines on them](https://docs.platypush.tech/en/latest/platypush/plugins/weather.darksky.html)
- [Create a custom single hub for Zigbee and Z-Wave smart devices](https://blog.platypush.tech/article/Transform-a-RaspberryPi-into-a-universal-Zigbee-and-Z-Wave-bridge)
- Control your smart home lights
- Control your favourite music player
- Interact with your voice assistant
- Get events from your Google or Facebook calendars
- Read data from your sensors and trigger custom events whenever they go above or below some custom thresholds
- Control the motors of your robot
- Send automated emails
- Synchronize the clipboards on your devices
- Control your smart switches
- Implement custom text-to-speech commands
- Build any kind of interaction with your Android device using Tasker
- Play local videos, YouTube videos and torrent links
- Get weather forecast for your location
- Build your own web dashboard with calendar, weather, news and music controls (basically, anything that has a Platypush web widget)
- ...and much more (basically, anything that comes with a [Platypush plugin](https://docs.platypush.tech/en/latest/plugins.html))
- ...and much more (basically, anything that comes with a [Platypush plugin](https://platypush.readthedocs.io/en/latest/plugins.html))
Imagine the ability of executing all the actions above through messages delivered through:
- A web interface
- A JSON-RPC API
- Raw TCP messages
- Web sockets
- [PushBullet](https://pushbullet.com)
- [Kafka](https://kafka.apache.org)
- [Redis](https://redis.io)
- [MQTT](https://mqtt.org)
- ...amd much more (basically, anything that comes with a [Platypush backend](https://platypush.readthedocs.io/en/latest/backends.html))
Imagine the ability of building custom event hooks to automatically trigger any actions:
- When your voice assistant recognizes some text
- When you start playing a new song
- When a new event is added to your calendar
- When a new article is published on your favourite feed
- When the weather conditions change
- When your press a [Flic button](https://flic.io) with a certain pattern
- When you receive a new push on your Pushbullet account
- When your GPS signal enters a certain area
- Whenever a new MIDI event is received (yes, you heard well :) )
- Whenever a sensor sends new data
- At a specific date or time
- ...and so on (basically, anything can send events that can be used to build hooks)
Imagine the ability of running the application, with lots of those bundled features, on any device that can comes with Python (_only compatible with version 3.5 and higher_). Platypush has been designed with performance in mind, it's been heavily tested on slower devices like Raspberry Pis, and it can run the web server features, multiple backends and plugins quite well even on a Raspberry Pi Zero - it's even been tested with some quite impressive performance on an older [Nokia N900](https://en.wikipedia.org/wiki/Nokia_N900), and of course you can run it on any laptop, desktop, server environment. It's been developed mainly with IoT in mind (and some of its features overlap with IoT frameworks like [Mozilla IoT](https://iot.mozilla.com) and [Android Things](https://developer.android.com/things/)), but nothing prevents you from automating any task on any device and environment.
To get started:
- [Wiki](https://github.com/BlackLight/platypush/wiki) for installation notes, quick start, examples and architecture reference
- [Read the docs](https://platypush.readthedocs.io/en/latest/) for a complete reference on the available plugins and backends
- [Medium articles](https://medium.com/tag/platypush/archive) that describe hands-on applications of platypush
## 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):
### [Plugins](https://docs.platypush.tech/en/latest/plugins.html)
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.
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:
# Groups that will be controlled by default
groups:
- Living Room
- Hall
```
### Actions
Plugins expose *actions*, that match one-on-one the plugin class methods denoted by `@action`, so it's very
straightforward to invoke plugin actions by just reading the plugin documentation. They can be invoked directly from
your own scripts or they can be sent to the platform through any supported channel as simple JSON messages:
```json
{
"type": "request",
"action": "light.hue.on",
"args": {
"lights": ["Entrance Bulb"]
}
}
```
### [Backends](https://docs.platypush.tech/en/latest/backends.html)
They are background services that either listen for messages on channels (like an
[HTTP backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
[MQTT instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
[Kafka instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html), a
[Websocket service](https://docs.platypush.tech/en/latest/platypush/backend/websocket.html),
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html) etc.) or monitor a device or a
service for events (like a [sensor](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html), a custom
[voice assistant](https://docs.platypush.tech/en/latest/platypush/backend/assistant.google.html), a bridge running on a
[Zigbee](https://docs.platypush.tech/en/latest/platypush/backend/zigbee.mqtt.html) or
[Z-Wave](https://docs.platypush.tech/en/latest/platypush/backend/zwave.html), an
[NFC card reader](https://docs.platypush.tech/en/latest/platypush/backend/nfc.html), a
[MIDI device](https://docs.platypush.tech/en/latest/platypush/backend/midi.html), a
[Telegram channel](https://docs.platypush.tech/en/latest/platypush/backend/chat.telegram.html), a
[Bluetooth scanner](https://docs.platypush.tech/en/latest/platypush/backend/bluetooth.scanner.ble.html) etc.).
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka, Websocket and TCP) then you can send requests
to these services in JSON format. For example, in the case of the HTTP backend:
```shell
# Get a token
curl -XPOST -H 'Content-Type: application/json' -d '
{
"username": "$YOUR_USER",
"password": "$YOUR_PASSWORD"
}' http://host:8008/auth
# Execute a request
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
{
"type": "request",
"action": "tts.say",
"args": {
"text": "This is a test"
}
}' http://host:8008/execute
```
### [Events](https://docs.platypush.tech/en/latest/events.html)
When a certain event occurs (e.g. a JSON request is received, or a
[Bluetooth device is connected](https://docs.platypush.tech/en/latest/platypush/events/bluetooth.html#platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent),
or a
[Flic button is pressed](https://docs.platypush.tech/en/latest/platypush/events/button.flic.html#platypush.message.event.button.flic.FlicButtonEvent),
or some
[speech is detected on the voice assistant service](https://docs.platypush.tech/en/latest/platypush/events/assistant.html#platypush.message.event.assistant.SpeechRecognizedEvent),
or an
[RSS feed has new items](https://docs.platypush.tech/en/latest/platypush/events/http.rss.html#platypush.message.event.http.rss.NewFeedEvent),
or a
[new email is received](https://docs.platypush.tech/en/latest/platypush/events/mail.html#platypush.message.event.mail.MailReceivedEvent),
or a
[new track is played](https://docs.platypush.tech/en/latest/platypush/events/music.html#platypush.message.event.music.NewPlayingTrackEvent),
or an
[NFC tag is detected](https://docs.platypush.tech/en/latest/platypush/events/nfc.html#platypush.message.event.nfc.NFCTagDetectedEvent),
or
[new sensor data is available](https://docs.platypush.tech/en/latest/platypush/events/sensor.html#platypush.message.event.sensor.SensorDataChangeEvent),
or
[a value of a Zigbee device changes](https://docs.platypush.tech/en/latest/platypush/events/zigbee.mqtt.html#platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent),
etc.), the associated backend will trigger an [event](https://docs.platypush.tech/en/latest/events.html).
### Hooks
Event hooks are custom pieces of logic that will be run when a certain event is triggered. Hooks are the glue that
connects events to actions, exposing a paradigm similar to IFTTT (_if a certain event happens then run these actions_).
They can declared as:
- Sections of the [`config.yaml`](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/config.yaml).
Example:
```yaml
event.hook.SearchSongVoiceCommand:
if:
type: platypush.message.event.assistant.SpeechRecognizedEvent
phrase: "play ${title} by ${artist}"
then:
- action: music.mpd.clear
- action: music.mpd.search
args:
filter:
artist: ${artist}
title: ${title}
- if ${len(output)}:
- action: music.mpd.play
args:
resource: ${output[0]['file']}
```
- Stand-alone Python scripts stored under `~/.config/platypush/scripts` and will be dynamically imported at start time.
[Example](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/hook.py):
```python
from platypush.event.hook import hook
from platypush.utils import run
from platypush.message.event.assistant import SpeechRecognizedEvent
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
def on_music_play_command(event, title=None, artist=None, **context):
results = run('music.mpd.search', filter={
'artist': artist,
'title': title,
})
if results:
run('music.mpd.play', results[0]['file'])
```
### Procedures
Procedures are pieces of custom logic that can be executed as atomic actions using `procedure.<name>` as an action name.
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` -
provided that the procedure is also imported in `~/.config/platypush/scripts/__init__.py` so it can be discovered by
the service.
YAML example for a procedure that can be executed when we arrive home and turns on the lights if the luminosity is lower
that a certain thresholds, says a welcome home message using the TTS engine and starts playing the music:
```yaml
procedure.at_home:
# Get luminosity data from a sensor - e.g. LTR559
- action: gpio.sensor.ltr559.get_data
# If it's lower than a certain threshold, turn on the lights
- if ${int(light or 0) < 110}:
- action: light.hue.on
# Say a welcome home message
- action: tts.google.say
args:
text: Welcome home
# Play the music
- action: music.mpd.play
```
Python example:
```python
# Content of ~/.config/platypush/scripts/home.py
from platypush.procedure import procedure
from platypush.utils import run
@procedure
def at_home(**context):
sensor_data = run('gpio.sensor.ltr559.get_data')
if sensor_data['light'] < 110:
run('light.hue.on')
run('tts.google.say', text='Welcome home')
run('music.mpd.play')
```
In either case, you can easily trigger the at-home procedure by sending an action request message to a backend - for
example, over the HTTP backend:
```shell
curl -XPOST -H 'Content-Type: application/json' -H "Authorization: Bearer $YOUR_TOKEN" -d '
{
"type": "request",
"action": "procedure.at_home"
}' http://host:8008/execute
```
### Cronjobs
Cronjobs are pieces of logic that will be run at regular intervals, expressed in crontab-compatible syntax.
They can be defined either in the `config.yaml` or as Python scripts stored under `~/.config/platypush/scripts` as
functions labelled by the `@cron` decorator.
Note that seconds are also supported (unlike the standard crontab definition), but, for back-compatibility with the
standard crontab format, they are at the end of the cron expression, so the expression is actually in the format
`<minute> <hour> <day_of_month> <month> <day_of_week> <second>`.
YAML example for a cronjob that is executed every 30 seconds and checks if a Bluetooth device is nearby:
```yaml
cron.check_bt_device:
cron_expression: '* * * * * */30'
actions:
- action: bluetooth.lookup_name
args:
addr: XX:XX:XX:XX:XX:XX
- if ${name}:
- action: procedure.on_device_on
- else:
- action: procedure.on_device_off
```
Python example:
```python
# Content of ~/.config/platypush/scripts/bt_cron.py
from platypush.cron import cron
from platypush.utils import run
@cron('* * * * * */30')
def check_bt_device(**context):
name = run('bluetooth.lookup_name').get('name')
if name:
# on_device_on logic here
else:
# on_device_off logic here
```
### The web interface
If [`backend.http`](https://docs.platypush.tech/en/latest/platypush/backend/http.html) is enabled then a web interface
will be provided by default on `http://host:8008/`. Besides using the `/execute` endpoint for running requests, the
built-in web server also provides a full-featured interface that groups together the controls for most of the plugins -
e.g. sensors, switches, music controls and search, media library and torrent management, lights, Zigbee/Z-Wave devices
and so on. The UI is responsive and mobile-friendly.
The web service also provides means for the user to create
[custom dashboards](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/conf/dashboard.xml) that can
be used to show information from multiple sources on a large screen.
## Installation
### System installation
Platypush uses Redis to deliver and store requests and temporary messages:
```yaml
# Example for Debian-based distributions
[sudo] apt-get install redis-server
# Enable and start the service
[sudo] systemctl enable redis
[sudo] systemctl start redis
```
To install the core platform:
* The `pip` way:
```shell
[sudo] pip3 install platypush
```
* The sources way:
```shell
git clone https://git.platypush.tech/platypush/platypush.git
cd platypush
[sudo] python3 setup.py install
```
Then install the extensions that you wish to use. There are a few ways to check the dependencies required by an
extension:
#### Check their `extras` name in [`extras_require` under `setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72).
If you follow this route then you can install the extra dependencies in one of the following ways:
1. `pip` installation:
```shell
[sudo] pip3 install 'platypush[extra1,extra2,extra3]'
```
2. Sources installation:
```shell
cd $DIR_TO_PLATYPUSH
[sudo] pip3 install '.[extra1,extra2,extra3]'
```
#### Check the dependencies/installation instructions reported under the plugin/backend documentation.
If you follow this route then simply run the commands listed in the plugin/backend documentation to get the dependencies
installed.
#### Check/uncomment the associated lines in [`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt).
If you follow this route then uncomment the lines in
[`requirements.txt`](https://git.platypush.tech/platypush/platypush/-/blob/master/requirements.txt) associated to the
plugins/backends that you want to use and run:
```shell
[sudo] pip3 install -r requirements.txt
```
After installing the dependencies, create a configuration file under `~/.config/platypush/config.yaml` (the application
can load the configuration from another location through the `-c` option) containing the configuration of the backend
and plugins that you want to use, and add any hooks and procedures for your use case.
You can then start the service by simply running:
```shell
platypush
```
It's advised to run it as a systemd service though - simply copy the provided
[`.service` file](https://git.platypush.tech/platypush/platypush/-/blob/master/examples/systemd/platypush.service) to
`~/.config/systemd/user`, check if the path of `platypush` matches the path where it's installed on your system, and
start the service via `systemctl`:
```shell
systemctl --user start platypush
```
### [Virtual environment installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-virtual-environment)
Platypush provides a script named `platyvenv` that can parse a `config.yaml` and automatically create a virtual
environment (under `~/.local/share/platypush/venv/<device_id>`) with all the dependencies required by the configured
integrations.
1. Create the environment from a configuration file:
```shell
platyvenv build -c /path/to/config.yaml
```
2. Start the service from the virtual environment:
```shell
# device_id matches either the hostname or the device_id in config.yaml
platyvenv start device_id
```
3. Stop the instance:
```shell
platyvenv stop device_id
```
4. Remove the instance:
```shell
platyvenv rm device_id
```
### [Docker installation](https://git.platypush.tech/platypush/platypush/-/wikis/Run-platypush-in-a-container)
You can also install Platypush in a container - the application provides a script named `platydock` that automatically
creates a container instance from a `config.yaml`:
1. Create the container from a configuration file:
```shell
platydock build -c /path/to/config.yaml
```
2. Start the container:
```shell
# device_id matches either the hostname or the device_id in config.yaml
platydock start device_id
```
3. Stop the instance:
```shell
platydock stop device_id
```
4. Remove the instance:
```shell
platydock rm device_id
```
## Mobile app
An [official Android app](https://f-droid.org/en/packages/tech.platypush.platypush/) is provided on the F-Droid store.
It allows to easily discover and manage multiple Platypush services on a network through the web interface, and it
easily brings the power of Platypush to your fingertips.
## Tests
To run the tests simply run `pytest` either from the project root folder or the `tests/` folder.
Or run the following command from the project root folder:
```shell
python -m tests
```
---
## Funding
If you use and love Platypush, please consider [buying me a coffee/beer](https://paypal.me/fabiomanganiello).
I've been working on Platypush all by myself in my spare time for the past few years, and I've made sure that it remains
open and free.
If you like this product, please consider supporting - I'm definitely not planning to get rich with this project, but
I'd love to have at least the costs for the server covered by users.
Issues and requests opened by donors will also be given priority over others.

View File

@ -119,7 +119,7 @@ EOF
pip install ${dep}
done
pip install --upgrade git+https://git.platypush.tech/platypush/platypush.git
pip install --upgrade git+https://github.com/BlackLight/platypush.git
echo "Platypush virtual environment prepared under $envdir"
}

View File

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

View File

@ -1,59 +0,0 @@
import importlib
import json
import os
import re
import sys
from typing import Union, List
from docutils import nodes
from docutils.parsers.rst import Directive
class SchemaDirective(Directive):
"""
Support for response/message schemas in the docs. Format: ``.. schema:: rel_path.SchemaClass(arg1=value1, ...)``,
where ``rel_path`` is the path of the schema relative to ``platypush/schemas``.
"""
has_content = True
_schema_regex = re.compile(r'^\s*(.+?)\s*(\((.+?)\))?\s*$')
_schemas_path = os.path.abspath(
os.path.join(
os.path.dirname(os.path.relpath(__file__)), '..', '..', '..', 'platypush', 'schemas'))
sys.path.insert(0, _schemas_path)
@staticmethod
def _get_field_value(field) -> str:
metadata = getattr(field, 'metadata', {})
return metadata.get('example', metadata.get('description', str(field.__class__.__name__).lower()))
def _parse_schema(self) -> Union[dict, List[dict]]:
m = self._schema_regex.match('\n'.join(self.content))
schema_module_name = '.'.join(['platypush.schemas', *(m.group(1).split('.')[:-1])])
schema_module = importlib.import_module(schema_module_name)
schema_class = getattr(schema_module, m.group(1).split('.')[-1])
schema_args = eval(f'dict({m.group(3)})')
schema = schema_class(**schema_args)
output = {
name: self._get_field_value(field)
for name, field in schema.fields.items()
if not field.load_only
}
return [output] if schema.many else output
def run(self):
content = json.dumps(self._parse_schema(), sort_keys=True, indent=2)
block = nodes.literal_block(content, content)
block['language'] = 'json'
return [block]
def setup(app):
app.add_directive('schema', SchemaDirective)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -21,10 +21,7 @@ Backends
platypush/backend/chat.telegram.rst
platypush/backend/clipboard.rst
platypush/backend/covid19.rst
platypush/backend/dbus.rst
platypush/backend/file.monitor.rst
platypush/backend/foursquare.rst
platypush/backend/github.rst
platypush/backend/google.fit.rst
platypush/backend/google.pubsub.rst
platypush/backend/gps.rst
@ -32,20 +29,15 @@ Backends
platypush/backend/http.poll.rst
platypush/backend/inotify.rst
platypush/backend/joystick.rst
platypush/backend/joystick.jstest.rst
platypush/backend/joystick.linux.rst
platypush/backend/kafka.rst
platypush/backend/light.hue.rst
platypush/backend/linode.rst
platypush/backend/log.http.rst
platypush/backend/mail.rst
platypush/backend/local.rst
platypush/backend/midi.rst
platypush/backend/mqtt.rst
platypush/backend/music.mopidy.rst
platypush/backend/music.mpd.rst
platypush/backend/music.snapcast.rst
platypush/backend/music.spotify.connect.rst
platypush/backend/nextcloud.rst
platypush/backend/nfc.rst
platypush/backend/nodered.rst
platypush/backend/ping.rst
@ -57,7 +49,6 @@ Backends
platypush/backend/sensor.arduino.rst
platypush/backend/sensor.battery.rst
platypush/backend/sensor.bme280.rst
platypush/backend/sensor.dht.rst
platypush/backend/sensor.distance.rst
platypush/backend/sensor.distance.vl53l1x.rst
platypush/backend/sensor.envirophat.rst
@ -75,12 +66,9 @@ Backends
platypush/backend/todoist.rst
platypush/backend/travisci.rst
platypush/backend/trello.rst
platypush/backend/weather.rst
platypush/backend/weather.buienradar.rst
platypush/backend/weather.darksky.rst
platypush/backend/weather.openweathermap.rst
platypush/backend/websocket.rst
platypush/backend/wiimote.rst
platypush/backend/zigbee.mqtt.rst
platypush/backend/zwave.rst
platypush/backend/zwave.mqtt.rst

View File

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

View File

@ -6,6 +6,7 @@ Events
:maxdepth: 2
:caption: Events:
platypush/events/.rst
platypush/events/adafruit.rst
platypush/events/alarm.rst
platypush/events/application.rst
@ -16,33 +17,28 @@ Events
platypush/events/chat.telegram.rst
platypush/events/clipboard.rst
platypush/events/covid19.rst
platypush/events/custom.rst
platypush/events/distance.rst
platypush/events/file.rst
platypush/events/foursquare.rst
platypush/events/geo.rst
platypush/events/github.rst
platypush/events/google.rst
platypush/events/google.fit.rst
platypush/events/google.pubsub.rst
platypush/events/gps.rst
platypush/events/http.rst
platypush/events/http.hook.rst
platypush/events/http.ota.booking.rst
platypush/events/http.rss.rst
platypush/events/inotify.rst
platypush/events/joystick.rst
platypush/events/kafka.rst
platypush/events/light.rst
platypush/events/linode.rst
platypush/events/log.http.rst
platypush/events/mail.rst
platypush/events/media.rst
platypush/events/midi.rst
platypush/events/mqtt.rst
platypush/events/music.rst
platypush/events/music.snapcast.rst
platypush/events/nextcloud.rst
platypush/events/nfc.rst
platypush/events/path.rst
platypush/events/ping.rst
platypush/events/pushbullet.rst
platypush/events/qrcode.rst

View File

@ -5,13 +5,13 @@ Welcome to the Platypush reference of available plugins, backends and event type
For more information on Platypush please check out:
* The `Gitlab page`_ of the project
* The `GitHub page`_ of the project
* The `online wiki`_ for quickstart and examples
* The `Blog articles`_ for inspiration on use-cases possible projects
* The `Medium stories`_ for inspiration about possible projects
.. _Gitlab page: https://git.platypush.tech/platypush/platypush
.. _online wiki: https://git.platypush.tech/platypush/platypush/-/wikis/home
.. _Blog articles: https://blog.platypush.tech
.. _GitHub page: https://github.com/BlackLight/platypush
.. _online wiki: https://github.com/BlackLight/platypush/wiki
.. _Medium stories: https://medium.com/tag/platypush/archive
.. toctree::
:maxdepth: 3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
``platypush.message.event.path``
================================
.. automodule:: platypush.message.event.path
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
``platypush.plugins.google.credentials``
========================================
.. automodule:: platypush.plugins.google.credentials
:members:

View File

@ -1,5 +0,0 @@
``platypush.plugins.gpio.sensor.dht``
=====================================
.. automodule:: platypush.plugins.gpio.sensor.dht
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
``platypush.plugins.media.search.local``
========================================
.. automodule:: platypush.plugins.media.search.local
:members:

View File

@ -0,0 +1,6 @@
``platypush.plugins.media.search.torrent``
==========================================
.. automodule:: platypush.plugins.media.search.torrent
:members:

View File

@ -0,0 +1,6 @@
``platypush.plugins.media.search.youtube``
==========================================
.. automodule:: platypush.plugins.media.search.youtube
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
``platypush.message.response.deepspeech``
=========================================
.. automodule:: platypush.message.response.deepspeech
:members:

View File

@ -20,21 +20,15 @@ Plugins
platypush/plugins/calendar.ical.rst
platypush/plugins/camera.rst
platypush/plugins/camera.android.ipcam.rst
platypush/plugins/camera.cv.rst
platypush/plugins/camera.ffmpeg.rst
platypush/plugins/camera.gstreamer.rst
platypush/plugins/camera.ir.mlx90640.rst
platypush/plugins/camera.pi.rst
platypush/plugins/chat.telegram.rst
platypush/plugins/clipboard.rst
platypush/plugins/config.rst
platypush/plugins/covid19.rst
platypush/plugins/csv.rst
platypush/plugins/db.rst
platypush/plugins/dbus.rst
platypush/plugins/dropbox.rst
platypush/plugins/esp.rst
platypush/plugins/ffmpeg.rst
platypush/plugins/file.rst
platypush/plugins/foursquare.rst
platypush/plugins/google.rst
@ -50,7 +44,6 @@ Plugins
platypush/plugins/gpio.sensor.rst
platypush/plugins/gpio.sensor.accelerometer.rst
platypush/plugins/gpio.sensor.bme280.rst
platypush/plugins/gpio.sensor.dht.rst
platypush/plugins/gpio.sensor.distance.rst
platypush/plugins/gpio.sensor.distance.vl53l1x.rst
platypush/plugins/gpio.sensor.envirophat.rst
@ -61,6 +54,7 @@ Plugins
platypush/plugins/graphite.rst
platypush/plugins/homeseer.rst
platypush/plugins/http.request.rst
platypush/plugins/http.request.ota.booking.rst
platypush/plugins/http.request.rss.rst
platypush/plugins/http.webpage.rst
platypush/plugins/ifttt.rst
@ -68,20 +62,14 @@ Plugins
platypush/plugins/inspect.rst
platypush/plugins/kafka.rst
platypush/plugins/lastfm.rst
platypush/plugins/lcd.rst
platypush/plugins/lcd.gpio.rst
platypush/plugins/lcd.i2c.rst
platypush/plugins/light.rst
platypush/plugins/light.hue.rst
platypush/plugins/linode.rst
platypush/plugins/logger.rst
platypush/plugins/luma.oled.rst
platypush/plugins/mail.rst
platypush/plugins/mail.imap.rst
platypush/plugins/mail.smtp.rst
platypush/plugins/media.rst
platypush/plugins/media.chromecast.rst
platypush/plugins/media.gstreamer.rst
platypush/plugins/media.ctrl.rst
platypush/plugins/media.kodi.rst
platypush/plugins/media.mplayer.rst
platypush/plugins/media.mpv.rst
@ -97,21 +85,17 @@ Plugins
platypush/plugins/music.rst
platypush/plugins/music.mpd.rst
platypush/plugins/music.snapcast.rst
platypush/plugins/nextcloud.rst
platypush/plugins/nmap.rst
platypush/plugins/otp.rst
platypush/plugins/pihole.rst
platypush/plugins/ping.rst
platypush/plugins/printer.cups.rst
platypush/plugins/pushbullet.rst
platypush/plugins/pwm.pca9685.rst
platypush/plugins/qrcode.rst
platypush/plugins/redis.rst
platypush/plugins/rtorrent.rst
platypush/plugins/sensor.rst
platypush/plugins/serial.rst
platypush/plugins/shell.rst
platypush/plugins/smartthings.rst
platypush/plugins/sound.rst
platypush/plugins/ssh.rst
platypush/plugins/stt.rst
@ -119,10 +103,9 @@ Plugins
platypush/plugins/stt.picovoice.hotword.rst
platypush/plugins/stt.picovoice.speech.rst
platypush/plugins/switch.rst
platypush/plugins/switch.switchbot.rst
platypush/plugins/switch.tplink.rst
platypush/plugins/switch.wemo.rst
platypush/plugins/switchbot.rst
platypush/plugins/switchbot.bluetooth.rst
platypush/plugins/system.rst
platypush/plugins/tcp.rst
platypush/plugins/tensorflow.rst
@ -133,19 +116,15 @@ Plugins
platypush/plugins/tts.rst
platypush/plugins/tts.google.rst
platypush/plugins/tv.samsung.ws.rst
platypush/plugins/twilio.rst
platypush/plugins/udp.rst
platypush/plugins/user.rst
platypush/plugins/utils.rst
platypush/plugins/variable.rst
platypush/plugins/weather.rst
platypush/plugins/video.torrentcast.rst
platypush/plugins/weather.buienradar.rst
platypush/plugins/weather.darksky.rst
platypush/plugins/weather.openweathermap.rst
platypush/plugins/websocket.rst
platypush/plugins/wiimote.rst
platypush/plugins/zeroconf.rst
platypush/plugins/zigbee.mqtt.rst
platypush/plugins/zwave.rst
platypush/plugins/zwave._base.rst
platypush/plugins/zwave.mqtt.rst

1
docs/wiki Submodule

@ -0,0 +1 @@
Subproject commit 6c0e65ccfe020bf6ce2eca43c387e827d4764c12

View File

@ -16,21 +16,21 @@
# Using multiple files is encouraged in the case of large configurations
# that can easily end up in a messy config.yaml file, as they help you
# keep your configuration more organized.
#include:
# - include/logging.yaml
# - include/media.yaml
# - include/sensors.yaml
include:
- include/logging.yaml
- include/media.yaml
- include/sensors.yaml
# platypush logs on stdout by default. You can use the logging section to specify
# an alternative file or change the logging level.
#logging:
# filename: ~/.local/log/platypush/platypush.log
# level: INFO
logging:
filename: ~/.local/log/platypush/platypush.log
level: INFO
# The device_id is used by many components of platypush and it should uniquely
# identify a device in your network. If nothing is specified then the hostname
# will be used.
#device_id: my_device
device_id: myname
## --
## Plugin configuration examples
@ -40,10 +40,10 @@
# a plugin class. The methods of the class with @action annotation will
# be exported as runnable actions, while the __init__ parameters are
# configuration attributes that you can initialize in your config.yaml.
# Plugin classes are documented at https://docs.platypush.tech/en/latest/plugins.html
# Plugin classes are documented at https://platypush.readthedocs.io/en/latest/plugins.html
#
# In this example we'll configure the light.hue plugin, see
# https://docs.platypush.tech/en/latest/platypush/plugins/light.hue.html
# https://platypush.readthedocs.io/en/latest/platypush/plugins/light.hue.html
# for reference. You can easily install the required dependencies for the plugin through
# pip install 'platypush[hue]'
light.hue:
@ -54,14 +54,14 @@ light.hue:
- Living Room
# Example configuration of music.mpd plugin, see
# https://docs.platypush.tech/en/latest/platypush/plugins/music.mpd.html
# https://platypush.readthedocs.io/en/latest/platypush/plugins/music.mpd.html
# You can easily install the dependencies through pip install 'platypush[mpd]'
music.mpd:
host: localhost
port: 6600
# Example configuration of media.chromecast plugin, see
# https://docs.platypush.tech/en/latest/platypush/plugins/media.chromecast.html
# https://platypush.readthedocs.io/en/latest/platypush/plugins/media.chromecast.html
# You can easily install the dependencies through pip install 'platypush[chromecast]'
media.chromecast:
chromecast: Living Room TV
@ -69,16 +69,25 @@ media.chromecast:
# Plugins with empty configuration can also be explicitly enabled by specifying
# enabled=True or disabled=False (it's a good practice if you want the
# corresponding web panel to be enabled, if available)
camera.pi:
camera:
enabled: True
# Support for last.fm scrobbling. Install dependencies with 'pip install "platypush[lastfm]"
lastfm:
api_key: your_api_key
api_secret: your_api_secret
username: your_username
password: your_password
# Support for calendars - in this case Google and Facebook calendars
# Installing the dependencies: pip install 'platypush[ical,google]'
calendar:
calendars:
- type: platypush.plugins.google.calendar.GoogleCalendarPlugin
- type: platypush.plugins.calendar.ical.CalendarIcalPlugin
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
-
type: platypush.plugins.google.calendar.GoogleCalendarPlugin
-
type: platypush.plugins.calendar.ical.CalendarIcalPlugin
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
## --
## Backends configuration examples
@ -88,10 +97,10 @@ calendar:
# to happen and either trigger events or provide additional services on top of platypush.
# Just like plugins, backends are classes whose configuration matches one-to-one the
# supported parameters on the __init__ methods. You can check the documentation for the
# available backends here: https://docs.platypush.tech/en/latest/backends.html.
# available backends here: https://platypush.readthedocs.io/en/latest/backends.html.
# Moreover, most of the backends will generate events that you can react to through custom
# event hooks. Check here for the events documentation:
# https://docs.platypush.tech/en/latest/events.html
# https://platypush.readthedocs.io/en/latest/events.html
#
# You may usually want to enable the HTTP backend, as it provides many useful features on
# top of platypush. Among those:
@ -109,14 +118,40 @@ calendar:
backend.http:
# Listening port
port: 8008
# Websocket port
websocket_port: 8009
# Through resource_dirs you can specify external folders whose content can be accessed on
# the web server through a custom URL. In the case below we have a Dropbox folder containing
# our pictures and we mount it to the '/carousel' endpoint.
resource_dirs:
carousel: /mnt/hd/photos/carousel
carousel: ~/Dropbox/Photos/carousel
# Dashboard configuration. The dashboard is a collection of widgets and it's organized in
# multiple rows. Each rows can be split in 12 columns. Therefore 'columns: 12' will make
# a widget span over the whole row, while 'columns: 6' will make a widget take half the
# horizontal space of a column.
dashboard:
widgets:
-
widget: calendar
columns: 6
-
widget: music
columns: 3
-
widget: date-time-weather
columns: 3
-
widget: image-carousel
columns: 6
images_path: ~/Dropbox/Photos/carousel
refresh_seconds: 15
-
widget: rss-news
# Requires backend.http.poll to be enabled with some
# RSS sources and write them to sqlite db
columns: 6
limit: 25
db: "sqlite:////home/user/.local/share/platypush/feeds/rss.db"
# The HTTP poll backend is a versatile backend that can monitor for HTTP-based resources and
# trigger events whenever new entries are available. In the example below we show how to use
@ -125,33 +160,35 @@ backend.http:
# Install the required dependencies through 'pip install "platypush[rss,db]"'
backend.http.poll:
requests:
- type: platypush.backend.http.request.rss.RssUpdates # HTTP poll type (RSS)
# Remote URL
url: http://www.theguardian.com/rss/world
# Custom title
title: The Guardian - World News
# How often we should check for changes
poll_seconds: 600
# Maximum number of new entries to be processed
max_entries: 10
- type: platypush.backend.http.request.rss.RssUpdates
url: http://www.physorg.com/rss-feed
title: Phys.org
poll_seconds: 600
max_entries: 10
- type: platypush.backend.http.request.rss.RssUpdates
url: http://feeds.feedburner.com/Techcrunch
title: Tech Crunch
poll_seconds: 600
max_entries: 10
- type: platypush.backend.http.request.rss.RssUpdates
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
title: The New York Times
poll_seconds: 300
max_entries: 10
-
# HTTP poll type (RSS)
type: platypush.backend.http.request.rss.RssUpdates
# Remote URL
url: http://www.theguardian.com/rss/world
# Custom title
title: The Guardian - World News
# How often we should check for changes
poll_seconds: 600
# Maximum number of new entries to be processed
max_entries: 10
-
type: platypush.backend.http.request.rss.RssUpdates
url: http://www.physorg.com/rss-feed
title: Phys.org
poll_seconds: 600
max_entries: 10
-
type: platypush.backend.http.request.rss.RssUpdates
url: http://feeds.feedburner.com/Techcrunch
title: Tech Crunch
poll_seconds: 600
max_entries: 10
-
type: platypush.backend.http.request.rss.RssUpdates
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
title: The New York Times
poll_seconds: 300
max_entries: 10
# MQTT backend. Installed required dependencies through 'pip install "platypush[mqtt]"'
backend.mqtt:
@ -159,15 +196,15 @@ backend.mqtt:
host: mqtt-server
# By default the backend will listen for messages on the platypush_bus_mq/device_id
# topic, but you can change the prefix using the topic attribute
# topic: MyBus
topic: my_platypush_bus
# Raw TCP socket backend. It can run commands sent as JSON over telnet or netcat
#backend.tcp:
# port: 3333
backend.tcp:
port: 3333
# Websocket backend. Install required dependencies through 'pip install "platypush[http]"'
#backend.websocket:
# port: 8765
backend.websocket:
port: 8765
## --
## Assistant configuration examples
@ -217,12 +254,9 @@ backend.assistant.snowboy:
assistant.echo:
audio_player: mplayer
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant-legacy]"'
assistant.google:
enabled: True
backend.assistant.google:
enabled: True
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant]"'
assistant.google.pushtotalk:
language: en-US
## --
## Procedure examples
@ -293,14 +327,14 @@ procedure.outside_home:
procedure.send_request(target, action, args):
- action: mqtt.send_message
args:
topic: platypush_bus_mq/${target}
topic: my_platypush_bus/${target}
host: mqtt-server
port: 1883
msg:
type: request
target: ${target}
action: ${action}
args: ${args}
args: "${context.get('args', {}}"
## --
## Event hook examples
@ -345,8 +379,10 @@ event.hook.SearchSongVoiceCommand:
- action: music.mpd.search
args:
filter:
artist: ${artist}
title: ${title}
- artist
- ${artist}
- any
- ${title}
# Play the first search result
- action: music.mpd.play

View File

@ -1,33 +0,0 @@
<!-- Dashboard templates are stored as ~/.config/platypush/dashboards/<name>.xml and can be accessed on
http://<host>:8008/dashboard/<name>. A dashboard can show a custom set of widgets on a screen - e.g. calendar
events, media information, photo carousels, sensors data, weather forecast and news headlines. The available
widgets are stored as Vue.js templates under `platypush/backend/http/webapp/src/components/widgets`. -->
<Dashboard>
<!-- Display the following widgets on the same row. Each row consists of 12 columns.
You can specify the width of each widget either through class name (e.g. col-6 means
6 columns out of 12, e.g. half the size of the row) or inline style
(e.g. `style="width: 50%"`). -->
<Row>
<!-- Show a calendar widget with the upcoming events. It requires the `calendar` plugin to
be enabled and configured. -->
<Calendar class="col-6" />
<!-- Show the current track and other playback info. It requires `music.mpd` plugin or any
other music plugin enabled. -->
<Music class="col-3" />
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
<DateTimeWeather class="col-3" />
</Row>
<!-- Display the following widgets on a second row -->
<Row>
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
`http.poll` backend -->
<RssNews class="col-6" db="sqlite:////path/to/your/rss.db" />
</Row>
</Dashboard>

View File

@ -1,43 +0,0 @@
# A more versatile way to define event hooks than the YAML format of `config.yaml` is through native Python scripts.
# You can define hooks as simple Python functions that use the `platypush.event.hook.hook` decorator to specify on
# which event type they should be called, and optionally on which event attribute values.
#
# Event hooks should be stored in Python files under `~/.config/platypush/scripts`. All the functions that use the
# @hook decorator will automatically be discovered and imported as event hooks into the platform at runtime.
# `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
from platypush.utils import run
# @hook decorator
from platypush.event.hook import hook
# Event types that you want to react to
from platypush.message.event.assistant import ConversationStartEvent, SpeechRecognizedEvent
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
def on_music_play_command(event, title=None, artist=None, **context):
"""
This function will be executed when a SpeechRecognizedEvent with `phrase="play the music"` is triggered.
`event` contains the event object and `context` any key-value info from the running context.
Note that in this specific case we can leverage the token-extraction feature of SpeechRecognizedEvent through
${} that operates on regex-like principles to extract any text that matches the pattern into context variables.
"""
results = run('music.mpd.search', filter={
'artist': artist,
'title': title,
})
if results:
run('music.mpd.play', results[0]['file'])
else:
run('tts.say', "I can't find any music matching your query")
@hook(ConversationStartEvent)
def on_conversation_start(event, **context):
"""
A simple hook that gets invoked when a new conversation starts with a voice assistant and simply pauses the music
to make sure that your speech is properly detected.
"""
run('music.mpd.pause_if_playing')

View File

@ -16,17 +16,18 @@ from .context import register_backends
from .cron.scheduler import CronScheduler
from .event.processor import EventProcessor
from .logger import Logger
from .message.event import Event
from .message.event.application import ApplicationStartedEvent
from .message.event import Event, StopEvent
from .message.event.application import ApplicationStartedEvent, ApplicationStoppedEvent
from .message.request import Request
from .message.response import Response
from .utils import set_thread_name
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
__version__ = '0.21.1'
__author__ = 'Fabio Manganiello <blacklight86@gmail.com>'
__version__ = '0.13.1'
logger = logging.getLogger('platypush')
LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)
class Daemon:
@ -41,9 +42,6 @@ class Daemon:
# - plugins will post the responses they process
bus = None
# Default bus queue name
_default_redis_queue = 'platypush/bus'
pidfile = None
# backend_name => backend_obj map
@ -53,7 +51,7 @@ class Daemon:
n_tries = 2
def __init__(self, config_file=None, pidfile=None, requests_to_process=None,
no_capture_stdout=False, no_capture_stderr=False, redis_queue=None):
no_capture_stdout=False, no_capture_stderr=False):
"""
Constructor
Params:
@ -67,7 +65,6 @@ class Daemon:
capture by the logging system
no_capture_stderr -- Set to true if you want to disable the stderr
capture by the logging system
redis_queue -- Name of the (Redis) queue used for dispatching messages (default: platypush/bus).
"""
if pidfile:
@ -75,21 +72,15 @@ class Daemon:
with open(self.pidfile, 'w') as f:
f.write(str(os.getpid()))
self.redis_queue = redis_queue or self._default_redis_queue
self.config_file = config_file
Config.init(self.config_file)
logging.basicConfig(**Config.get('logging'))
redis_conf = Config.get('backend.redis') or {}
self.bus = RedisBus(redis_queue=self.redis_queue, on_message=self.on_message(),
**redis_conf.get('redis_args', {}))
self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr
self.event_processor = EventProcessor()
self.requests_to_process = requests_to_process
self.processed_requests = 0
self.cron_scheduler = None
@classmethod
def build_from_cmdline(cls, args):
@ -115,17 +106,11 @@ class Daemon:
help="Set this flag if you have max stack depth " +
"exceeded errors so stderr won't be captured by " +
"the logging system")
parser.add_argument('--redis-queue', dest='redis_queue',
required=False, action='store_true',
default=cls._default_redis_queue,
help="Name of the Redis queue to be used to internally deliver messages "
"(default: platypush/bus)")
opts, args = parser.parse_known_args(args)
return cls(config_file=opts.config, pidfile=opts.pidfile,
no_capture_stdout=opts.no_capture_stdout,
no_capture_stderr=opts.no_capture_stderr,
redis_queue=opts.redis_queue)
no_capture_stderr=opts.no_capture_stderr)
def on_message(self):
"""
@ -143,17 +128,20 @@ class Daemon:
try:
msg.execute(n_tries=self.n_tries)
except PermissionError:
logger.info('Dropped unauthorized request: {}'.format(msg))
LOGGER.info('Dropped unauthorized request: {}'.format(msg))
self.processed_requests += 1
if self.requests_to_process \
and self.processed_requests >= self.requests_to_process:
self.stop_app()
elif isinstance(msg, Response):
logger.info('Received response: {}'.format(msg))
LOGGER.info('Received response: {}'.format(msg))
elif isinstance(msg, StopEvent) and msg.targets_me():
LOGGER.info('Received STOP event: {}'.format(msg))
self.stop_app()
elif isinstance(msg, Event):
if not msg.disable_logging:
logger.info('Received event: {}'.format(msg))
LOGGER.info('Received event: {}'.format(msg))
self.event_processor.process_event(msg)
return _f
@ -162,20 +150,22 @@ class Daemon:
""" Stops the backends and the bus """
for backend in self.backends.values():
backend.stop()
self.bus.stop()
if self.cron_scheduler:
self.cron_scheduler.stop()
def run(self):
def start(self):
""" Start the daemon """
if not self.no_capture_stdout:
sys.stdout = Logger(logger.info)
sys.stdout = Logger(LOGGER.info)
if not self.no_capture_stderr:
sys.stderr = Logger(logger.warning)
sys.stderr = Logger(LOGGER.warning)
set_thread_name('platypush')
logger.info('---- Starting platypush v.{}'.format(__version__))
print('---- Starting platypush v.{}'.format(__version__))
redis_conf = Config.get('backend.redis') or {}
self.bus = RedisBus(on_message=self.on_message(),
**redis_conf.get('redis_args', {}))
# Initialize the backends and link them to the bus
self.backends = register_backends(bus=self.bus, global_scope=True)
@ -186,8 +176,7 @@ class Daemon:
# Start the cron scheduler
if Config.get_cronjobs():
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())
self.cron_scheduler.start()
CronScheduler(jobs=Config.get_cronjobs()).start()
self.bus.post(ApplicationStartedEvent())
@ -195,8 +184,9 @@ class Daemon:
try:
self.bus.poll()
except KeyboardInterrupt:
logger.info('SIGINT received, terminating application')
LOGGER.info('SIGINT received, terminating application')
finally:
self.bus.post(ApplicationStoppedEvent())
self.stop_app()
@ -204,8 +194,9 @@ def main():
"""
Platypush daemon main
"""
app = Daemon.build_from_cmdline(sys.argv[1:])
app.run()
app.start()
# vim:sw=4:ts=4:et:

View File

@ -6,22 +6,23 @@
import logging
import re
import socket
import threading
import time
from threading import Thread, Event as ThreadEvent, get_ident
from typing import Optional, Dict
from threading import Thread
from typing import Optional
from platypush.bus import Bus
from platypush.config import Config
from platypush.context import get_backend
from platypush.message.event.zeroconf import ZeroconfServiceAddedEvent, ZeroconfServiceRemovedEvent
from platypush.utils import set_timeout, clear_timeout, \
get_redis_queue_name_by_message, set_thread_name, get_backend_name_by_class
get_redis_queue_name_by_message, set_thread_name
from platypush import __version__
from platypush.event import EventGenerator
from platypush.message import Message
from platypush.message.event import Event
from platypush.message.event import Event, StopEvent
from platypush.message.request import Request
from platypush.message.response import Response
@ -61,9 +62,10 @@ class Backend(Thread, EventGenerator):
self.poll_seconds = float(poll_seconds) if poll_seconds else None
self.device_id = Config.get('device_id')
self.thread_id = None
self._stop_event = ThreadEvent()
self._should_stop = False
self._stop_event = threading.Event()
self._kwargs = kwargs
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
self.logger = logging.getLogger(self.__class__.__name__)
self.zeroconf = None
self.zeroconf_info = None
@ -101,8 +103,12 @@ class Backend(Thread, EventGenerator):
self.stop()
return
msg.backend = self # Augment message to be able to process responses
self.bus.post(msg)
if isinstance(msg, StopEvent) and msg.targets_me():
self.logger.info('Received STOP event on {}'.format(self.__class__.__name__))
self._should_stop = True
else:
msg.backend = self # Augment message to be able to process responses
self.bus.post(msg)
def _is_expected_response(self, msg):
""" Internal only - returns true if we are expecting for a response
@ -219,7 +225,7 @@ class Backend(Thread, EventGenerator):
def run(self):
""" Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """
self.thread_id = get_ident()
self.thread_id = threading.get_ident()
set_thread_name(self._thread_name)
if not callable(self.loop):
return
@ -257,22 +263,25 @@ class Backend(Thread, EventGenerator):
def on_stop(self):
""" Callback invoked when the process stops """
pass
self.unregister_service()
def stop(self):
""" Stops the backend thread by sending a STOP event on its bus """
def _async_stop():
evt = StopEvent(target=self.device_id, origin=self.device_id,
thread_id=self.thread_id)
self.send_message(evt)
self._stop_event.set()
self.unregister_service()
self.on_stop()
Thread(target=_async_stop).start()
def should_stop(self):
return self._stop_event.is_set()
return self._should_stop
def wait_stop(self, timeout=None) -> bool:
return self._stop_event.wait(timeout)
def wait_stop(self, timeout=None):
self._stop_event.wait(timeout)
def _get_redis(self):
import redis
@ -312,35 +321,12 @@ class Backend(Thread, EventGenerator):
s.close()
return addr
def register_service(self,
port: Optional[int] = None,
name: Optional[str] = None,
srv_type: Optional[str] = None,
srv_name: Optional[str] = None,
udp: bool = False,
properties: Optional[Dict] = None):
def register_service(self, port: Optional[int] = None, name: Optional[str] = None, udp: bool = False):
"""
Initialize the Zeroconf service configuration for this backend.
:param port: Service listen port (default: the backend ``port`` attribute if available, or ``None``).
:param name: Service short name (default: backend name).
:param srv_type: Service type (default: ``_platypush-{name}._{proto}.local.``).
:param srv_name: Full service name (default: ``{hostname or device_id}.{type}``).
:param udp: Set to True if this is a UDP service.
:param properties: Extra properties to be passed on the service. Default:
.. code-block:: json
{
"name": "Platypush",
"vendor": "Platypush",
"version": "{platypush_version}"
}
"""
try:
from zeroconf import ServiceInfo, Zeroconf
from platypush.plugins.zeroconf import ZeroconfListener
except ImportError:
self.logger.warning('zeroconf package not available, service discovery will be disabled.')
return
@ -350,52 +336,32 @@ class Backend(Thread, EventGenerator):
'name': 'Platypush',
'vendor': 'Platypush',
'version': __version__,
**(properties or {}),
}
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
srv_type = srv_type or '_platypush-{name}._{proto}.local.'.format(name=name, proto='udp' if udp else 'tcp')
srv_name = srv_name or '{host}.{type}'.format(host=self.device_id, type=srv_type)
srv_type = '_platypush-{name}._{proto}.local.'.format(name=name, proto='udp' if udp else 'tcp')
srv_name = '{host}.{type}'.format(host=self.device_id, type=srv_type)
if port:
srv_port = port
else:
srv_port = self.port if hasattr(self, 'port') else None
self.zeroconf_info = ServiceInfo(srv_type, srv_name,
addresses=[socket.inet_aton(self._get_ip())],
port=srv_port,
weight=0,
priority=0,
properties=srv_desc)
if not self.zeroconf_info:
self.logger.warning('Could not register Zeroconf service')
return
self.zeroconf_info = ServiceInfo(srv_type, srv_name, socket.inet_aton(self._get_ip()),
srv_port, 0, 0, srv_desc)
self.zeroconf.register_service(self.zeroconf_info)
self.bus.post(ZeroconfServiceAddedEvent(service_type=srv_type, service_name=srv_name,
service_info=ZeroconfListener.parse_service_info(self.zeroconf_info)))
self.bus.post(ZeroconfServiceAddedEvent(service_type=srv_type, service_name=srv_name))
def unregister_service(self):
"""
Unregister the Zeroconf service configuration if available.
"""
if self.zeroconf and self.zeroconf_info:
try:
self.zeroconf.unregister_service(self.zeroconf_info)
except Exception as e:
self.logger.warning('Could not register Zeroconf service {}: {}: {}'.format(
self.zeroconf_info.name, type(e).__name__, str(e)))
if self.zeroconf:
self.zeroconf.close()
if self.zeroconf_info:
self.bus.post(ZeroconfServiceRemovedEvent(service_type=self.zeroconf_info.type,
service_name=self.zeroconf_info.name))
else:
self.bus.post(ZeroconfServiceRemovedEvent(service_type=None, service_name=None))
self.zeroconf.unregister_service(self.zeroconf_info)
self.zeroconf.close()
self.bus.post(ZeroconfServiceRemovedEvent(service_type=self.zeroconf_info.type,
service_name=self.zeroconf_info.name))
self.zeroconf_info = None
self.zeroconf = None

View File

@ -42,7 +42,6 @@ class AdafruitIoBackend(Backend):
if not plugin:
raise RuntimeError('Adafruit IO plugin not configured')
# noinspection PyProtectedMember
self._client = MQTTClient(plugin._username, plugin._key)
self._client.on_connect = self.on_connect()
self._client.on_disconnect = self.on_disconnect()
@ -53,25 +52,18 @@ class AdafruitIoBackend(Backend):
for feed in self.feeds:
client.subscribe(feed)
self.bus.post(ConnectedEvent())
return _handler
def on_disconnect(self):
def _handler(client):
self.bus.post(DisconnectedEvent())
return _handler
def on_message(self, msg):
# noinspection PyUnusedLocal
def on_message(self):
def _handler(client, feed, data):
try:
data = float(data)
except Exception as e:
self.logger.debug('Not a number: {}: {}'.format(data, e))
try: data = float(data)
except: pass
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
return _handler
def run(self):
@ -89,4 +81,5 @@ class AdafruitIoBackend(Backend):
self.logger.exception(e)
self._client = None
# vim:sw=4:ts=4:et:

View File

@ -1,5 +1,4 @@
import datetime
import enum
import os
import time
import threading
@ -7,7 +6,7 @@ import threading
from typing import Optional, Union, Dict, Any, List
import croniter
from dateutil.tz import gettz
import enum
from platypush.backend import Backend
from platypush.context import get_bus, get_plugin
@ -55,20 +54,18 @@ class Alarm:
self._runtime_snooze_interval = snooze_interval
def get_next(self) -> float:
now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable]
now = time.time()
try:
cron = croniter.croniter(self.when, now)
return cron.get_next()
except (AttributeError, croniter.CroniterBadCronError):
try:
timestamp = datetime.datetime.fromisoformat(self.when).replace(
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
timestamp = datetime.datetime.fromisoformat(self.when).timestamp()
except (TypeError, ValueError):
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable]
datetime.timedelta(seconds=int(self.when)))
timestamp = (datetime.datetime.now() + datetime.timedelta(seconds=int(self.when))).timestamp()
return timestamp.timestamp() if timestamp >= now else None
return timestamp if timestamp >= now else None
def is_enabled(self):
return self._enabled

View File

@ -21,10 +21,12 @@ class AssistantGoogleBackend(AssistantBackend):
It listens for voice commands and post conversation events on the bus.
**WARNING**: The Google Assistant library used by this backend has officially been deprecated:
https://developers.google.com/assistant/sdk/reference/library/python/. This backend still works on most of the
devices where I use it, but its correct functioning is not guaranteed as the assistant library is no longer
maintained.
**WARNING**: This backend is deprecated, as the underlying Google Assistant
library has been deprecated too: https://developers.google.com/assistant/sdk/reference/library/python/
The old library might still work on some systems but its proper functioning
is not guaranteed.
Please use the Snowboy backend for hotword detection and the Google Assistant
push-to-talk plugin for assistant interaction instead.
Triggers:

View File

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

View File

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

View File

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

View File

@ -19,10 +19,6 @@ class CameraPiBackend(Backend):
* **picamera** (``pip install picamera``)
* **redis** (``pip install redis``) for inter-process communication with the camera process
This backend is **DEPRECATED**. Use the plugin :class:`platypush.plugins.camera.pi.CameraPiPlugin` instead to run
Pi camera actions. If you want to start streaming the camera on application start then simply create an event hook
on :class:`platypush.message.event.application.ApplicationStartedEvent` that runs ``camera.pi.start_streaming``.
"""
class CameraAction(Enum):
@ -34,7 +30,7 @@ class CameraPiBackend(Backend):
return self.value == other
# noinspection PyUnresolvedReferences,PyPackageRequirements
def __init__(self, listen_port, bind_address='0.0.0.0', x_resolution=640, y_resolution=480,
def __init__(self, listen_port, x_resolution=640, y_resolution=480,
redis_queue='platypush/camera/pi',
start_recording_on_startup=True,
framerate=24, hflip=False, vflip=False,
@ -49,17 +45,13 @@ class CameraPiBackend(Backend):
:param listen_port: Port where the camera process will provide the video output while recording
:type listen_port: int
:param bind_address: Bind address (default: 0.0.0.0).
:type bind_address: str
"""
super().__init__(**kwargs)
self.bind_address = bind_address
self.listen_port = listen_port
self.server_socket = socket.socket()
self.server_socket.bind((self.bind_address, self.listen_port))
self.server_socket.bind(('0.0.0.0', self.listen_port))
self.server_socket.listen(0)
import picamera
@ -126,7 +118,7 @@ class CameraPiBackend(Backend):
while True:
self.camera.wait_recording(2)
else:
while not self.should_stop():
while True:
connection = self.server_socket.accept()[0].makefile('wb')
self.logger.info('Accepted client connection on port {}'.format(self.listen_port))
@ -138,13 +130,13 @@ class CameraPiBackend(Backend):
self.logger.info('Client closed connection')
try:
self.stop_recording()
except Exception as e:
self.logger.warning('Could not stop recording: {}'.format(str(e)))
except:
pass
try:
connection.close()
except Exception as e:
self.logger.warning('Could not close connection: {}'.format(str(e)))
except:
pass
self.send_camera_action(self.CameraAction.START_RECORDING)

View File

@ -26,7 +26,6 @@ class ClipboardBackend(Backend):
self._last_text: Optional[str] = None
def run(self):
self.logger.info('Started clipboard monitor backend')
while not self.should_stop():
text = pyperclip.paste()
if text and text != self._last_text:
@ -35,6 +34,5 @@ class ClipboardBackend(Backend):
self._last_text = text
time.sleep(0.1)
self.logger.info('Stopped clipboard monitor backend')
# vim:sw=4:ts=4:et:

View File

@ -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:

View File

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

View File

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

View File

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

View File

@ -1,226 +0,0 @@
import datetime
import os
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
from sqlalchemy.orm import sessionmaker, scoped_session
from platypush.backend import Backend
from platypush.config import Config
from platypush.message.event.github import GithubPushEvent, GithubCommitCommentEvent, GithubCreateEvent, \
GithubDeleteEvent, GithubEvent, GithubForkEvent, GithubWikiEvent, GithubIssueCommentEvent, GithubIssueEvent, \
GithubMemberEvent, GithubPublicEvent, GithubPullRequestEvent, GithubPullRequestReviewCommentEvent, \
GithubReleaseEvent, GithubSponsorshipEvent, GithubWatchEvent
Base = declarative_base()
Session = scoped_session(sessionmaker())
class GithubResource(Base):
"""
Models the GithubLastEvent table, containing the timestamp where a certain URL was last checked.
"""
__tablename__ = 'GithubLastEvent'
uri = Column(String, primary_key=True)
last_updated_at = Column(DateTime)
class GithubBackend(Backend):
"""
This backend monitors for notifications and events either on Github user, organization or repository level.
You'll need a Github personal access token to use the service. To get one:
- Access your Github profile settings
- Select *Developer Settings*
- Select *Personal access tokens*
- Click *Generate new token*
This backend requires the following permissions:
- ``repo``
- ``notifications``
- ``read:org`` if you want to access repositories on organization level.
Triggers:
- :class:`platypush.message.event.github.GithubPushEvent` when a new push is created.
- :class:`platypush.message.event.github.GithubCommitCommentEvent` when a new commit comment is created.
- :class:`platypush.message.event.github.GithubCreateEvent` when a tag or branch is created.
- :class:`platypush.message.event.github.GithubDeleteEvent` when a tag or branch is deleted.
- :class:`platypush.message.event.github.GithubForkEvent` when a user forks a repository.
- :class:`platypush.message.event.github.GithubWikiEvent` when new activity happens on a repository wiki.
- :class:`platypush.message.event.github.GithubIssueCommentEvent` when new activity happens on an issue comment.
- :class:`platypush.message.event.github.GithubIssueEvent` when new repository issue activity happens.
- :class:`platypush.message.event.github.GithubMemberEvent` when new repository collaborators activity happens.
- :class:`platypush.message.event.github.GithubPublicEvent` when a repository goes public.
- :class:`platypush.message.event.github.GithubPullRequestEvent` when new pull request related activity happens.
- :class:`platypush.message.event.github.GithubPullRequestReviewCommentEvent` when activity happens on a pull
request commit.
- :class:`platypush.message.event.github.GithubReleaseEvent` when a new release happens.
- :class:`platypush.message.event.github.GithubSponsorshipEvent` when new sponsorship related activity happens.
- :class:`platypush.message.event.github.GithubWatchEvent` when someone stars/starts watching a repository.
- :class:`platypush.message.event.github.GithubEvent` for any event that doesn't fall in the above categories
(``event_type`` will be set accordingly).
"""
_base_url = 'https://api.github.com'
def __init__(self, user: str, user_token: str, repos: Optional[List[str]] = None, org: Optional[str] = None,
poll_seconds: int = 60, max_events_per_scan: Optional[int] = 10, *args, **kwargs):
"""
If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level.
:param user: Github username.
:param user_token: Github personal access token.
:param repos: List of repos to be monitored - if a list is provided then only these repositories will be
monitored for events. Repositories should be passed in the format ``username/repository``.
:param org: Organization to be monitored - if provided then only this organization will be monitored for events.
:param poll_seconds: How often the backend should check for new events, in seconds (default: 60).
:param max_events_per_scan: Maximum number of events per resource that will be triggered if there is a large
number of events/notification since the last check (default: 10). Specify 0 or null for no limit.
"""
super().__init__(*args, **kwargs)
self._last_text: Optional[str] = None
self.user = user
self.user_token = user_token
self.repos = repos or []
self.org = org
self.poll_seconds = poll_seconds
self.db_lock = threading.RLock()
self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'github')
self.dbfile = os.path.join(self.workdir, 'github.db')
self.max_events_per_scan = max_events_per_scan
os.makedirs(os.path.dirname(self.dbfile), exist_ok=True)
self._init_db()
def _request(self, uri: str, method: str = 'get') -> dict:
method = getattr(requests, method.lower())
return method(self._base_url + uri, auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'}).json()
def _init_db(self):
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
Base.metadata.create_all(engine)
Session.configure(bind=engine)
@staticmethod
def _to_datetime(time_string: str) -> datetime.datetime:
""" Convert ISO 8061 string format with leading 'Z' into something understandable by Python """
return datetime.datetime.fromisoformat(time_string[:-1] + '+00:00')
@staticmethod
def _get_or_create_resource(uri: str, session: Session) -> GithubResource:
record = session.query(GithubResource).filter_by(uri=uri).first()
if record is None:
record = GithubResource(uri=uri)
session.add(record)
session.commit()
return record
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
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
with self.db_lock:
session = Session()
record = self._get_or_create_resource(uri=uri, session=session)
record.last_updated_at = last_updated_at
session.add(record)
session.commit()
@classmethod
def _parse_event(cls, event: dict) -> GithubEvent:
event_mapping = {
'PushEvent': GithubPushEvent,
'CommitCommentEvent': GithubCommitCommentEvent,
'CreateEvent': GithubCreateEvent,
'DeleteEvent': GithubDeleteEvent,
'ForkEvent': GithubForkEvent,
'GollumEvent': GithubWikiEvent,
'IssueCommentEvent': GithubIssueCommentEvent,
'IssuesEvent': GithubIssueEvent,
'MemberEvent': GithubMemberEvent,
'PublicEvent': GithubPublicEvent,
'PullRequestEvent': GithubPullRequestEvent,
'PullRequestReviewCommentEvent': GithubPullRequestReviewCommentEvent,
'ReleaseEvent': GithubReleaseEvent,
'SponsorshipEvent': GithubSponsorshipEvent,
'WatchEvent': GithubWatchEvent,
}
event_type = event_mapping[event['type']] if event['type'] in event_mapping else GithubEvent
return event_type(event_type=event['type'], actor=event['actor'], repo=event.get('repo', {}),
payload=event['payload'], created_at=cls._to_datetime(event['created_at']))
def _events_monitor(self, uri: str, method: str = 'get'):
def thread():
while not self.should_stop():
try:
events = self._request(uri, method)
if not events:
continue
last_event_time = self._get_last_event_time(uri)
new_last_event_time = last_event_time
fired_events = []
for event in events:
if self.max_events_per_scan and len(fired_events) >= self.max_events_per_scan:
break
event_time = self._to_datetime(event['created_at'])
if last_event_time and event_time <= last_event_time:
break
if not new_last_event_time or event_time > new_last_event_time:
new_last_event_time = event_time
fired_events.append(self._parse_event(event))
for event in fired_events:
self.bus.post(event)
self._update_last_event_time(uri=uri, last_updated_at=new_last_event_time)
except Exception as e:
self.logger.warning('Encountered exception while fetching events from {}: {}'.format(
uri, str(e)))
self.logger.exception(e)
finally:
if self.wait_stop(timeout=self.poll_seconds):
break
return thread
def run(self):
self.logger.info('Starting Github backend')
monitors = []
if self.repos:
for repo in self.repos:
monitors.append(threading.Thread(target=self._events_monitor('/networks/{repo}/events'.format(repo=repo))))
if self.org:
monitors.append(threading.Thread(target=self._events_monitor('/orgs/{org}/events'.format(org=self.org))))
if not (self.repos or self.org):
monitors.append(threading.Thread(target=self._events_monitor('/users/{user}/events'.format(user=self.user))))
for monitor in monitors:
monitor.start()
self.logger.info('Started Github backend')
for monitor in monitors:
monitor.join()
self.logger.info('Github backend terminated')
# vim:sw=4:ts=4:et:

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