Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
|
bb5df2a557 |
2135 changed files with 22007 additions and 150065 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,7 +3,7 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
build/
|
build/
|
||||||
/dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
package.sh
|
package.sh
|
||||||
.pypirc
|
.pypirc
|
||||||
|
@ -17,5 +17,3 @@ platypush/backend/http/static/js/lib/vue.js
|
||||||
platypush/notebooks
|
platypush/notebooks
|
||||||
platypush/requests
|
platypush/requests
|
||||||
/http-client.env.json
|
/http-client.env.json
|
||||||
/platypush/backend/http/static/css/dist
|
|
||||||
/tests/etc/dashboards
|
|
||||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -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"]
|
[submodule "platypush/plugins/gpio/sensor/ir/mlx90640/lib"]
|
||||||
path = platypush/plugins/camera/ir/mlx90640/lib
|
path = platypush/plugins/camera/ir/mlx90640/lib
|
||||||
url = https://github.com/pimoroni/mlx90640-library
|
url = https://github.com/pimoroni/mlx90640-library
|
||||||
|
[submodule "webext"]
|
||||||
|
path = webext
|
||||||
|
url = https://github.com/BlackLight/platypush-webext.git
|
||||||
|
|
7
.readthedocs.yml
Normal file
7
.readthedocs.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
build:
|
||||||
|
image: latest
|
||||||
|
|
||||||
|
python:
|
||||||
|
version: 3.6
|
||||||
|
setup_py_install: true
|
||||||
|
|
9
.travis.requirements
Normal file
9
.travis.requirements
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pyyaml
|
||||||
|
requests
|
||||||
|
flask
|
||||||
|
redis
|
||||||
|
python-dateutil
|
||||||
|
websockets
|
||||||
|
bcrypt
|
||||||
|
sqlalchemy
|
||||||
|
croniter
|
22
.travis.yml
Normal file
22
.travis.yml
Normal 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
|
||||||
|
|
177
CHANGELOG.md
177
CHANGELOG.md
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -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
|
include platypush/plugins/http/webpage/mercury-parser.js
|
||||||
|
|
553
README.md
553
README.md
|
@ -1,509 +1,68 @@
|
||||||
Platypush
|
Platypush
|
||||||
=========
|
=========
|
||||||
|
|
||||||
[![Build Status](https://ci.platypush.tech/status.svg)](https://ci.platypush.tech/latest.log)
|
[![Build Status](https://travis-ci.org/BlackLight/platypush.svg?branch=master)](https://travis-ci.org/BlackLight/platypush)
|
||||||
[![Documentation Status](https://ci.platypush.tech/docs/status.svg)](https://ci.platypush.tech/docs/latest.log)
|
[![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/)
|
[![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.
|
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.
|
||||||
|
|
||||||
- Extensive documentation for all the available integrations and messages [is available](https://docs.platypush.tech/).
|
|
||||||
|
|
||||||
- If you have issues/feature requests/enhancement ideas please [create an issue](https://git.platypush.tech/platypush/platypush/-/issues).
|
|
||||||
|
|
||||||
- A [Reddit channel](https://www.reddit.com/r/platypush) is also available for more general questions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Platypush is a general-purpose extensible platform for automation and integration across multiple services and devices.
|
|
||||||
|
|
||||||
It enables users to create their own self-hosted pieces of automation based on events (*if this happens then do that*)
|
|
||||||
and it provides a comprehensive and customizable user interface that collects everything you need to visualize and
|
|
||||||
control under one roof.
|
|
||||||
|
|
||||||
It takes some concepts from [IFTTT](https://ifttt.com), [Tasker](https://tasker.joaoapps.com/),
|
|
||||||
[Microsoft Flow](https://flow.microsoft.com), [PushBullet](https://pushbullet.com) and
|
|
||||||
[Home Assistant](https://www.home-assistant.io/) to provide an environment where the user can easily connect things
|
|
||||||
together.
|
|
||||||
|
|
||||||
Its ideal home is a single-board computer like a RaspberryPi that you can configure to orchestrate any home automation
|
|
||||||
and cloud automation in your own living room or garage, but it can easily run on any device that can run a Python
|
|
||||||
interpreter, and the bar for the hardware requirements is very low as well - I use it to run pieces of automation on
|
|
||||||
devices as powerful as a RaspberryPi Zero or an old Nokia N900 with Linux.
|
|
||||||
|
|
||||||
You can use Platypush to do things like:
|
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 smart home lights
|
||||||
- [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)
|
- Control your favourite music player
|
||||||
- [Create custom and privacy-secure voice assistants that run custom hooks on your phrases](https://blog.platypush.tech/article/Build-custom-voice-assistants)
|
- Interact with your voice assistant
|
||||||
- Build integrations between [sensors](https://docs.platypush.tech/en/latest/platypush/backend/sensor.html),
|
- Get events from your Google or Facebook calendars
|
||||||
[cameras](https://docs.platypush.tech/en/latest/platypush/plugins/camera.pi.html),
|
- Read data from your sensors and trigger custom events whenever they go above or below some custom thresholds
|
||||||
[microphones](https://docs.platypush.tech/en/latest/platypush/plugins/sound.html) and
|
- Control the motors of your robot
|
||||||
[machine learning models](https://docs.platypush.tech/en/latest/platypush/plugins/tensorflow.html) to create smart
|
- Send automated emails
|
||||||
pieces of automation for e.g.
|
- Synchronize the clipboards on your devices
|
||||||
[people detection](https://blog.platypush.tech/article/Detect-people-with-a-RaspberryPi-a-thermal-camera-Platypush-and-a-pinch-of-machine-learning)
|
- Control your smart switches
|
||||||
or [sound detection](https://blog.platypush.tech/article/Create-your-smart-baby-monitor-with-Platypush-and-Tensorflow)
|
- Implement custom text-to-speech commands
|
||||||
- [Get events from your Google or Facebook calendars](https://docs.platypush.tech/en/latest/platypush/plugins/calendar.html)
|
- Build any kind of interaction with your Android device using Tasker
|
||||||
- [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)
|
- Play local videos, YouTube videos and torrent links
|
||||||
- [Control and automate a self-built robot](https://docs.platypush.tech/en/latest/platypush/plugins/gpio.zeroborg.html)
|
- Get weather forecast for your location
|
||||||
- [Deliver automated newsletters from custom RSS digests](https://blog.platypush.tech/article/Deliver-customized-newsletters-from-RSS-feeds-with-Platypush)
|
|
||||||
- [Synchronize the clipboards on your devices](https://docs.platypush.tech/en/latest/platypush/plugins/clipboard.html)
|
|
||||||
- [Control your smart switches](https://docs.platypush.tech/en/latest/platypush/plugins/switch.html)
|
|
||||||
- [Implement automated custom text-to-speech routines](https://docs.platypush.tech/en/latest/platypush/plugins/tts.html)
|
|
||||||
- [Build any kind of interactions and automation routines with your Android device using Tasker](https://blog.platypush.tech/article/How-to-build-your-personal-infrastructure-for-data-collection-and-visualization)
|
|
||||||
- Play [local videos](https://docs.platypush.tech/en/latest/platypush/plugins/media.mpv.html), YouTube videos and torrent media from any device and service, to any device
|
|
||||||
- [Get weather forecast events for your location and build automation routines on them](https://docs.platypush.tech/en/latest/platypush/plugins/weather.darksky.html)
|
|
||||||
- [Create a custom single hub for Zigbee and Z-Wave smart devices](https://blog.platypush.tech/article/Transform-a-RaspberryPi-into-a-universal-Zigbee-and-Z-Wave-bridge)
|
|
||||||
- Build your own web dashboard with calendar, weather, news and music controls (basically, anything that has a Platypush web widget)
|
- 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.
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ EOF
|
||||||
pip install ${dep}
|
pip install ${dep}
|
||||||
done
|
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"
|
echo "Platypush virtual environment prepared under $envdir"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -21,10 +21,7 @@ Backends
|
||||||
platypush/backend/chat.telegram.rst
|
platypush/backend/chat.telegram.rst
|
||||||
platypush/backend/clipboard.rst
|
platypush/backend/clipboard.rst
|
||||||
platypush/backend/covid19.rst
|
platypush/backend/covid19.rst
|
||||||
platypush/backend/dbus.rst
|
|
||||||
platypush/backend/file.monitor.rst
|
|
||||||
platypush/backend/foursquare.rst
|
platypush/backend/foursquare.rst
|
||||||
platypush/backend/github.rst
|
|
||||||
platypush/backend/google.fit.rst
|
platypush/backend/google.fit.rst
|
||||||
platypush/backend/google.pubsub.rst
|
platypush/backend/google.pubsub.rst
|
||||||
platypush/backend/gps.rst
|
platypush/backend/gps.rst
|
||||||
|
@ -32,20 +29,15 @@ Backends
|
||||||
platypush/backend/http.poll.rst
|
platypush/backend/http.poll.rst
|
||||||
platypush/backend/inotify.rst
|
platypush/backend/inotify.rst
|
||||||
platypush/backend/joystick.rst
|
platypush/backend/joystick.rst
|
||||||
platypush/backend/joystick.jstest.rst
|
|
||||||
platypush/backend/joystick.linux.rst
|
|
||||||
platypush/backend/kafka.rst
|
platypush/backend/kafka.rst
|
||||||
platypush/backend/light.hue.rst
|
platypush/backend/light.hue.rst
|
||||||
platypush/backend/linode.rst
|
platypush/backend/linode.rst
|
||||||
platypush/backend/log.http.rst
|
platypush/backend/local.rst
|
||||||
platypush/backend/mail.rst
|
|
||||||
platypush/backend/midi.rst
|
platypush/backend/midi.rst
|
||||||
platypush/backend/mqtt.rst
|
platypush/backend/mqtt.rst
|
||||||
platypush/backend/music.mopidy.rst
|
platypush/backend/music.mopidy.rst
|
||||||
platypush/backend/music.mpd.rst
|
platypush/backend/music.mpd.rst
|
||||||
platypush/backend/music.snapcast.rst
|
platypush/backend/music.snapcast.rst
|
||||||
platypush/backend/music.spotify.connect.rst
|
|
||||||
platypush/backend/nextcloud.rst
|
|
||||||
platypush/backend/nfc.rst
|
platypush/backend/nfc.rst
|
||||||
platypush/backend/nodered.rst
|
platypush/backend/nodered.rst
|
||||||
platypush/backend/ping.rst
|
platypush/backend/ping.rst
|
||||||
|
@ -57,7 +49,6 @@ Backends
|
||||||
platypush/backend/sensor.arduino.rst
|
platypush/backend/sensor.arduino.rst
|
||||||
platypush/backend/sensor.battery.rst
|
platypush/backend/sensor.battery.rst
|
||||||
platypush/backend/sensor.bme280.rst
|
platypush/backend/sensor.bme280.rst
|
||||||
platypush/backend/sensor.dht.rst
|
|
||||||
platypush/backend/sensor.distance.rst
|
platypush/backend/sensor.distance.rst
|
||||||
platypush/backend/sensor.distance.vl53l1x.rst
|
platypush/backend/sensor.distance.vl53l1x.rst
|
||||||
platypush/backend/sensor.envirophat.rst
|
platypush/backend/sensor.envirophat.rst
|
||||||
|
@ -75,12 +66,9 @@ Backends
|
||||||
platypush/backend/todoist.rst
|
platypush/backend/todoist.rst
|
||||||
platypush/backend/travisci.rst
|
platypush/backend/travisci.rst
|
||||||
platypush/backend/trello.rst
|
platypush/backend/trello.rst
|
||||||
platypush/backend/weather.rst
|
|
||||||
platypush/backend/weather.buienradar.rst
|
platypush/backend/weather.buienradar.rst
|
||||||
platypush/backend/weather.darksky.rst
|
platypush/backend/weather.darksky.rst
|
||||||
platypush/backend/weather.openweathermap.rst
|
|
||||||
platypush/backend/websocket.rst
|
platypush/backend/websocket.rst
|
||||||
platypush/backend/wiimote.rst
|
platypush/backend/wiimote.rst
|
||||||
platypush/backend/zigbee.mqtt.rst
|
platypush/backend/zigbee.mqtt.rst
|
||||||
platypush/backend/zwave.rst
|
platypush/backend/zwave.rst
|
||||||
platypush/backend/zwave.mqtt.rst
|
|
||||||
|
|
|
@ -18,13 +18,12 @@ import sys
|
||||||
# import os
|
# import os
|
||||||
# import sys
|
# import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
sys.path.insert(0, os.path.abspath("./_ext"))
|
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'Platypush'
|
project = 'platypush'
|
||||||
copyright = '2017-2021, Fabio Manganiello'
|
copyright = '2017-2019, Fabio Manganiello'
|
||||||
author = 'Fabio Manganiello'
|
author = 'Fabio Manganiello'
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
|
@ -51,7 +50,6 @@ extensions = [
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
'sphinx.ext.githubpages',
|
'sphinx.ext.githubpages',
|
||||||
'sphinx_rtd_theme',
|
'sphinx_rtd_theme',
|
||||||
'sphinx_marshmallow',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
@ -88,8 +86,7 @@ pygments_style = 'sphinx'
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
# html_theme = 'haiku'
|
# html_theme = 'haiku'
|
||||||
# html_theme = 'sphinx_rtd_theme'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
html_theme = 'sphinx_material'
|
|
||||||
|
|
||||||
html_domain_indices = True
|
html_domain_indices = True
|
||||||
|
|
||||||
|
@ -97,52 +94,7 @@ html_domain_indices = True
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
#
|
#
|
||||||
html_theme_options = {
|
# html_theme_options = {}
|
||||||
'nav_title': 'Platypush documentation',
|
|
||||||
'repo_url': 'https://git.platypush.tech/platypush/platypush',
|
|
||||||
'repo_name': 'Source code',
|
|
||||||
'repo_type': 'gitlab',
|
|
||||||
'color_primary': 'green',
|
|
||||||
'color_accent': 'light-green',
|
|
||||||
'logo_icon': '🕮',
|
|
||||||
'nav_links': [
|
|
||||||
{
|
|
||||||
'href': 'https://platypush.tech/',
|
|
||||||
'title': 'Homepage',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://blog.platypush.tech/',
|
|
||||||
'title': 'Blog',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://git.platypush.tech/platypush/platypush',
|
|
||||||
'title': 'Repository',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://git.platypush.tech/platypush/platypush/-/wikis/home',
|
|
||||||
'title': 'Wiki',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://chrome.google.com/webstore/detail/platypush/aphldjclndofhflbbdnmpejbjgomkbie',
|
|
||||||
'title': 'Chrome Extension',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://addons.mozilla.org/en-US/firefox/addon/platypush/',
|
|
||||||
'title': 'Firefox Extension',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'href': 'https://f-droid.org/en/packages/tech.platypush.platypush/',
|
|
||||||
'title': 'Android App',
|
|
||||||
'internal': False,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# 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,
|
# 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',
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||||
# 'searchbox.html']``.
|
# 'searchbox.html']``.
|
||||||
#
|
#
|
||||||
html_sidebars = {
|
# html_sidebars = {}
|
||||||
'**': ['logo-text.html', 'globaltoc.html', 'localtoc.html', 'searchbox.html']
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ---------------------------------------------
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
|
@ -301,20 +252,6 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'pandas',
|
'pandas',
|
||||||
'samsungtvws',
|
'samsungtvws',
|
||||||
'paramiko',
|
'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('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
@ -327,5 +264,3 @@ def skip(app, what, name, obj, skip, options):
|
||||||
def setup(app):
|
def setup(app):
|
||||||
app.connect("autodoc-skip-member", skip)
|
app.connect("autodoc-skip-member", skip)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ Events
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Events:
|
:caption: Events:
|
||||||
|
|
||||||
|
platypush/events/.rst
|
||||||
platypush/events/adafruit.rst
|
platypush/events/adafruit.rst
|
||||||
platypush/events/alarm.rst
|
platypush/events/alarm.rst
|
||||||
platypush/events/application.rst
|
platypush/events/application.rst
|
||||||
|
@ -16,33 +17,28 @@ Events
|
||||||
platypush/events/chat.telegram.rst
|
platypush/events/chat.telegram.rst
|
||||||
platypush/events/clipboard.rst
|
platypush/events/clipboard.rst
|
||||||
platypush/events/covid19.rst
|
platypush/events/covid19.rst
|
||||||
platypush/events/custom.rst
|
|
||||||
platypush/events/distance.rst
|
platypush/events/distance.rst
|
||||||
platypush/events/file.rst
|
|
||||||
platypush/events/foursquare.rst
|
platypush/events/foursquare.rst
|
||||||
platypush/events/geo.rst
|
platypush/events/geo.rst
|
||||||
platypush/events/github.rst
|
|
||||||
platypush/events/google.rst
|
platypush/events/google.rst
|
||||||
platypush/events/google.fit.rst
|
platypush/events/google.fit.rst
|
||||||
platypush/events/google.pubsub.rst
|
platypush/events/google.pubsub.rst
|
||||||
platypush/events/gps.rst
|
platypush/events/gps.rst
|
||||||
platypush/events/http.rst
|
platypush/events/http.rst
|
||||||
platypush/events/http.hook.rst
|
platypush/events/http.hook.rst
|
||||||
|
platypush/events/http.ota.booking.rst
|
||||||
platypush/events/http.rss.rst
|
platypush/events/http.rss.rst
|
||||||
platypush/events/inotify.rst
|
|
||||||
platypush/events/joystick.rst
|
platypush/events/joystick.rst
|
||||||
platypush/events/kafka.rst
|
platypush/events/kafka.rst
|
||||||
platypush/events/light.rst
|
platypush/events/light.rst
|
||||||
platypush/events/linode.rst
|
platypush/events/linode.rst
|
||||||
platypush/events/log.http.rst
|
|
||||||
platypush/events/mail.rst
|
|
||||||
platypush/events/media.rst
|
platypush/events/media.rst
|
||||||
platypush/events/midi.rst
|
platypush/events/midi.rst
|
||||||
platypush/events/mqtt.rst
|
platypush/events/mqtt.rst
|
||||||
platypush/events/music.rst
|
platypush/events/music.rst
|
||||||
platypush/events/music.snapcast.rst
|
platypush/events/music.snapcast.rst
|
||||||
platypush/events/nextcloud.rst
|
|
||||||
platypush/events/nfc.rst
|
platypush/events/nfc.rst
|
||||||
|
platypush/events/path.rst
|
||||||
platypush/events/ping.rst
|
platypush/events/ping.rst
|
||||||
platypush/events/pushbullet.rst
|
platypush/events/pushbullet.rst
|
||||||
platypush/events/qrcode.rst
|
platypush/events/qrcode.rst
|
||||||
|
@ -65,6 +61,5 @@ Events
|
||||||
platypush/events/web.widget.rst
|
platypush/events/web.widget.rst
|
||||||
platypush/events/wiimote.rst
|
platypush/events/wiimote.rst
|
||||||
platypush/events/zeroborg.rst
|
platypush/events/zeroborg.rst
|
||||||
platypush/events/zeroconf.rst
|
|
||||||
platypush/events/zigbee.mqtt.rst
|
platypush/events/zigbee.mqtt.rst
|
||||||
platypush/events/zwave.rst
|
platypush/events/zwave.rst
|
||||||
|
|
|
@ -5,13 +5,13 @@ Welcome to the Platypush reference of available plugins, backends and event type
|
||||||
|
|
||||||
For more information on Platypush please check out:
|
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 `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
|
.. _GitHub page: https://github.com/BlackLight/platypush
|
||||||
.. _online wiki: https://git.platypush.tech/platypush/platypush/-/wikis/home
|
.. _online wiki: https://github.com/BlackLight/platypush/wiki
|
||||||
.. _Blog articles: https://blog.platypush.tech
|
.. _Medium stories: https://medium.com/tag/platypush/archive
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 3
|
:maxdepth: 3
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.dbus``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.dbus
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.file.monitor``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.file.monitor
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.github``
|
|
||||||
============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.github
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.joystick.jstest``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.joystick.jstest
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.joystick.linux``
|
|
||||||
====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.joystick.linux
|
|
||||||
:members:
|
|
5
docs/source/platypush/backend/local.rst
Normal file
5
docs/source/platypush/backend/local.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.backend.local``
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. automodule:: platypush.backend.local
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.log.http``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.log.http
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.mail``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.mail
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.music.spotify.connect``
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.music.spotify.connect
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.nextcloud``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.nextcloud
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.sensor.dht``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.sensor.dht
|
|
||||||
:members:
|
|
5
docs/source/platypush/backend/stt.picovoice.rst
Normal file
5
docs/source/platypush/backend/stt.picovoice.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.backend.stt.picovoice``
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.backend.stt.picovoice
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.weather.openweathermap``
|
|
||||||
============================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.weather.openweathermap
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.weather``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.weather
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.backend.zwave.mqtt``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.zwave.mqtt
|
|
||||||
:members:
|
|
5
docs/source/platypush/events/.rst
Normal file
5
docs/source/platypush/events/.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.message.event``
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.custom``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.custom
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.file``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.file
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.github``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.github
|
|
||||||
:members:
|
|
5
docs/source/platypush/events/http.ota.booking.rst
Normal file
5
docs/source/platypush/events/http.ota.booking.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.message.event.http.ota.booking``
|
||||||
|
============================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.http.ota.booking
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.inotify``
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.inotify
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.log.http``
|
|
||||||
====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.log.http
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.mail``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.mail
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.nextcloud``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.nextcloud
|
|
||||||
:members:
|
|
6
docs/source/platypush/events/path.rst
Normal file
6
docs/source/platypush/events/path.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.message.event.path``
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.path
|
||||||
|
:members:
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.message.event.zeroconf``
|
|
||||||
====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.zeroconf
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.camera.cv``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.camera.cv
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.camera.ffmpeg``
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.camera.ffmpeg
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.camera.gstreamer``
|
|
||||||
======================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.camera.gstreamer
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.config``
|
|
||||||
============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.config
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.dbus``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.dbus
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.ffmpeg``
|
|
||||||
============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.ffmpeg
|
|
||||||
:members:
|
|
6
docs/source/platypush/plugins/google.credentials.rst
Normal file
6
docs/source/platypush/plugins/google.credentials.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.plugins.google.credentials``
|
||||||
|
========================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.google.credentials
|
||||||
|
:members:
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.gpio.sensor.dht``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.gpio.sensor.dht
|
|
||||||
:members:
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.http.request.ota.booking``
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.http.request.ota.booking
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.lcd.gpio``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.lcd.gpio
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.lcd.i2c``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.lcd.i2c
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.lcd``
|
|
||||||
=========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.lcd
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.luma.oled``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.luma.oled
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.mail.imap``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.mail.imap
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.mail``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.mail
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.mail.smtp``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.mail.smtp
|
|
||||||
:members:
|
|
5
docs/source/platypush/plugins/media.ctrl.rst
Normal file
5
docs/source/platypush/plugins/media.ctrl.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.media.ctrl``
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.media.ctrl
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.media.gstreamer``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.media.gstreamer
|
|
||||||
:members:
|
|
6
docs/source/platypush/plugins/media.search.local.rst
Normal file
6
docs/source/platypush/plugins/media.search.local.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.plugins.media.search.local``
|
||||||
|
========================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.media.search.local
|
||||||
|
:members:
|
||||||
|
|
6
docs/source/platypush/plugins/media.search.torrent.rst
Normal file
6
docs/source/platypush/plugins/media.search.torrent.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.plugins.media.search.torrent``
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.media.search.torrent
|
||||||
|
:members:
|
||||||
|
|
6
docs/source/platypush/plugins/media.search.youtube.rst
Normal file
6
docs/source/platypush/plugins/media.search.youtube.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.plugins.media.search.youtube``
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.media.search.youtube
|
||||||
|
:members:
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.nextcloud``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.nextcloud
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.pwm.pca9685``
|
|
||||||
=================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.pwm.pca9685
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.rtorrent``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.rtorrent
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.smartthings``
|
|
||||||
=================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.smartthings
|
|
||||||
:members:
|
|
5
docs/source/platypush/plugins/stt.picovoice.rst
Normal file
5
docs/source/platypush/plugins/stt.picovoice.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.stt.picovoice``
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.stt.picovoice
|
||||||
|
:members:
|
6
docs/source/platypush/plugins/switch.switchbot.rst
Normal file
6
docs/source/platypush/plugins/switch.switchbot.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.plugins.switch.switchbot``
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.switch.switchbot
|
||||||
|
:members:
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.switchbot.bluetooth``
|
|
||||||
=========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.switchbot.bluetooth
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.switchbot``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.switchbot
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.twilio``
|
|
||||||
============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.twilio
|
|
||||||
:members:
|
|
5
docs/source/platypush/plugins/video.torrentcast.rst
Normal file
5
docs/source/platypush/plugins/video.torrentcast.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.video.torrentcast``
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.video.torrentcast
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.weather.openweathermap``
|
|
||||||
============================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.weather.openweathermap
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.weather``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.weather
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.zeroconf``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.zeroconf
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.zwave._base``
|
|
||||||
=================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.zwave._base
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``platypush.plugins.zwave.mqtt``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.zwave.mqtt
|
|
||||||
:members:
|
|
5
docs/source/platypush/responses/deepspeech.rst
Normal file
5
docs/source/platypush/responses/deepspeech.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.message.response.deepspeech``
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.response.deepspeech
|
||||||
|
:members:
|
|
@ -20,21 +20,15 @@ Plugins
|
||||||
platypush/plugins/calendar.ical.rst
|
platypush/plugins/calendar.ical.rst
|
||||||
platypush/plugins/camera.rst
|
platypush/plugins/camera.rst
|
||||||
platypush/plugins/camera.android.ipcam.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.ir.mlx90640.rst
|
||||||
platypush/plugins/camera.pi.rst
|
platypush/plugins/camera.pi.rst
|
||||||
platypush/plugins/chat.telegram.rst
|
platypush/plugins/chat.telegram.rst
|
||||||
platypush/plugins/clipboard.rst
|
platypush/plugins/clipboard.rst
|
||||||
platypush/plugins/config.rst
|
|
||||||
platypush/plugins/covid19.rst
|
platypush/plugins/covid19.rst
|
||||||
platypush/plugins/csv.rst
|
platypush/plugins/csv.rst
|
||||||
platypush/plugins/db.rst
|
platypush/plugins/db.rst
|
||||||
platypush/plugins/dbus.rst
|
|
||||||
platypush/plugins/dropbox.rst
|
platypush/plugins/dropbox.rst
|
||||||
platypush/plugins/esp.rst
|
platypush/plugins/esp.rst
|
||||||
platypush/plugins/ffmpeg.rst
|
|
||||||
platypush/plugins/file.rst
|
platypush/plugins/file.rst
|
||||||
platypush/plugins/foursquare.rst
|
platypush/plugins/foursquare.rst
|
||||||
platypush/plugins/google.rst
|
platypush/plugins/google.rst
|
||||||
|
@ -50,7 +44,6 @@ Plugins
|
||||||
platypush/plugins/gpio.sensor.rst
|
platypush/plugins/gpio.sensor.rst
|
||||||
platypush/plugins/gpio.sensor.accelerometer.rst
|
platypush/plugins/gpio.sensor.accelerometer.rst
|
||||||
platypush/plugins/gpio.sensor.bme280.rst
|
platypush/plugins/gpio.sensor.bme280.rst
|
||||||
platypush/plugins/gpio.sensor.dht.rst
|
|
||||||
platypush/plugins/gpio.sensor.distance.rst
|
platypush/plugins/gpio.sensor.distance.rst
|
||||||
platypush/plugins/gpio.sensor.distance.vl53l1x.rst
|
platypush/plugins/gpio.sensor.distance.vl53l1x.rst
|
||||||
platypush/plugins/gpio.sensor.envirophat.rst
|
platypush/plugins/gpio.sensor.envirophat.rst
|
||||||
|
@ -61,6 +54,7 @@ Plugins
|
||||||
platypush/plugins/graphite.rst
|
platypush/plugins/graphite.rst
|
||||||
platypush/plugins/homeseer.rst
|
platypush/plugins/homeseer.rst
|
||||||
platypush/plugins/http.request.rst
|
platypush/plugins/http.request.rst
|
||||||
|
platypush/plugins/http.request.ota.booking.rst
|
||||||
platypush/plugins/http.request.rss.rst
|
platypush/plugins/http.request.rss.rst
|
||||||
platypush/plugins/http.webpage.rst
|
platypush/plugins/http.webpage.rst
|
||||||
platypush/plugins/ifttt.rst
|
platypush/plugins/ifttt.rst
|
||||||
|
@ -68,20 +62,13 @@ Plugins
|
||||||
platypush/plugins/inspect.rst
|
platypush/plugins/inspect.rst
|
||||||
platypush/plugins/kafka.rst
|
platypush/plugins/kafka.rst
|
||||||
platypush/plugins/lastfm.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.rst
|
||||||
platypush/plugins/light.hue.rst
|
platypush/plugins/light.hue.rst
|
||||||
platypush/plugins/linode.rst
|
platypush/plugins/linode.rst
|
||||||
platypush/plugins/logger.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.rst
|
||||||
platypush/plugins/media.chromecast.rst
|
platypush/plugins/media.chromecast.rst
|
||||||
platypush/plugins/media.gstreamer.rst
|
platypush/plugins/media.ctrl.rst
|
||||||
platypush/plugins/media.kodi.rst
|
platypush/plugins/media.kodi.rst
|
||||||
platypush/plugins/media.mplayer.rst
|
platypush/plugins/media.mplayer.rst
|
||||||
platypush/plugins/media.mpv.rst
|
platypush/plugins/media.mpv.rst
|
||||||
|
@ -97,21 +84,17 @@ Plugins
|
||||||
platypush/plugins/music.rst
|
platypush/plugins/music.rst
|
||||||
platypush/plugins/music.mpd.rst
|
platypush/plugins/music.mpd.rst
|
||||||
platypush/plugins/music.snapcast.rst
|
platypush/plugins/music.snapcast.rst
|
||||||
platypush/plugins/nextcloud.rst
|
|
||||||
platypush/plugins/nmap.rst
|
platypush/plugins/nmap.rst
|
||||||
platypush/plugins/otp.rst
|
platypush/plugins/otp.rst
|
||||||
platypush/plugins/pihole.rst
|
platypush/plugins/pihole.rst
|
||||||
platypush/plugins/ping.rst
|
platypush/plugins/ping.rst
|
||||||
platypush/plugins/printer.cups.rst
|
platypush/plugins/printer.cups.rst
|
||||||
platypush/plugins/pushbullet.rst
|
platypush/plugins/pushbullet.rst
|
||||||
platypush/plugins/pwm.pca9685.rst
|
|
||||||
platypush/plugins/qrcode.rst
|
platypush/plugins/qrcode.rst
|
||||||
platypush/plugins/redis.rst
|
platypush/plugins/redis.rst
|
||||||
platypush/plugins/rtorrent.rst
|
|
||||||
platypush/plugins/sensor.rst
|
platypush/plugins/sensor.rst
|
||||||
platypush/plugins/serial.rst
|
platypush/plugins/serial.rst
|
||||||
platypush/plugins/shell.rst
|
platypush/plugins/shell.rst
|
||||||
platypush/plugins/smartthings.rst
|
|
||||||
platypush/plugins/sound.rst
|
platypush/plugins/sound.rst
|
||||||
platypush/plugins/ssh.rst
|
platypush/plugins/ssh.rst
|
||||||
platypush/plugins/stt.rst
|
platypush/plugins/stt.rst
|
||||||
|
@ -119,10 +102,9 @@ Plugins
|
||||||
platypush/plugins/stt.picovoice.hotword.rst
|
platypush/plugins/stt.picovoice.hotword.rst
|
||||||
platypush/plugins/stt.picovoice.speech.rst
|
platypush/plugins/stt.picovoice.speech.rst
|
||||||
platypush/plugins/switch.rst
|
platypush/plugins/switch.rst
|
||||||
|
platypush/plugins/switch.switchbot.rst
|
||||||
platypush/plugins/switch.tplink.rst
|
platypush/plugins/switch.tplink.rst
|
||||||
platypush/plugins/switch.wemo.rst
|
platypush/plugins/switch.wemo.rst
|
||||||
platypush/plugins/switchbot.rst
|
|
||||||
platypush/plugins/switchbot.bluetooth.rst
|
|
||||||
platypush/plugins/system.rst
|
platypush/plugins/system.rst
|
||||||
platypush/plugins/tcp.rst
|
platypush/plugins/tcp.rst
|
||||||
platypush/plugins/tensorflow.rst
|
platypush/plugins/tensorflow.rst
|
||||||
|
@ -133,19 +115,14 @@ Plugins
|
||||||
platypush/plugins/tts.rst
|
platypush/plugins/tts.rst
|
||||||
platypush/plugins/tts.google.rst
|
platypush/plugins/tts.google.rst
|
||||||
platypush/plugins/tv.samsung.ws.rst
|
platypush/plugins/tv.samsung.ws.rst
|
||||||
platypush/plugins/twilio.rst
|
|
||||||
platypush/plugins/udp.rst
|
platypush/plugins/udp.rst
|
||||||
platypush/plugins/user.rst
|
platypush/plugins/user.rst
|
||||||
platypush/plugins/utils.rst
|
platypush/plugins/utils.rst
|
||||||
platypush/plugins/variable.rst
|
platypush/plugins/variable.rst
|
||||||
platypush/plugins/weather.rst
|
platypush/plugins/video.torrentcast.rst
|
||||||
platypush/plugins/weather.buienradar.rst
|
platypush/plugins/weather.buienradar.rst
|
||||||
platypush/plugins/weather.darksky.rst
|
platypush/plugins/weather.darksky.rst
|
||||||
platypush/plugins/weather.openweathermap.rst
|
|
||||||
platypush/plugins/websocket.rst
|
platypush/plugins/websocket.rst
|
||||||
platypush/plugins/wiimote.rst
|
platypush/plugins/wiimote.rst
|
||||||
platypush/plugins/zeroconf.rst
|
|
||||||
platypush/plugins/zigbee.mqtt.rst
|
platypush/plugins/zigbee.mqtt.rst
|
||||||
platypush/plugins/zwave.rst
|
platypush/plugins/zwave.rst
|
||||||
platypush/plugins/zwave._base.rst
|
|
||||||
platypush/plugins/zwave.mqtt.rst
|
|
||||||
|
|
1
docs/wiki
Submodule
1
docs/wiki
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6c0e65ccfe020bf6ce2eca43c387e827d4764c12
|
|
@ -16,21 +16,21 @@
|
||||||
# Using multiple files is encouraged in the case of large configurations
|
# 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
|
# that can easily end up in a messy config.yaml file, as they help you
|
||||||
# keep your configuration more organized.
|
# keep your configuration more organized.
|
||||||
#include:
|
include:
|
||||||
# - include/logging.yaml
|
- include/logging.yaml
|
||||||
# - include/media.yaml
|
- include/media.yaml
|
||||||
# - include/sensors.yaml
|
- include/sensors.yaml
|
||||||
|
|
||||||
# platypush logs on stdout by default. You can use the logging section to specify
|
# platypush logs on stdout by default. You can use the logging section to specify
|
||||||
# an alternative file or change the logging level.
|
# an alternative file or change the logging level.
|
||||||
#logging:
|
logging:
|
||||||
# filename: ~/.local/log/platypush/platypush.log
|
filename: ~/.local/log/platypush/platypush.log
|
||||||
# level: INFO
|
level: INFO
|
||||||
|
|
||||||
# The device_id is used by many components of platypush and it should uniquely
|
# 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
|
# identify a device in your network. If nothing is specified then the hostname
|
||||||
# will be used.
|
# will be used.
|
||||||
#device_id: my_device
|
device_id: myname
|
||||||
|
|
||||||
## --
|
## --
|
||||||
## Plugin configuration examples
|
## Plugin configuration examples
|
||||||
|
@ -40,10 +40,10 @@
|
||||||
# a plugin class. The methods of the class with @action annotation will
|
# a plugin class. The methods of the class with @action annotation will
|
||||||
# be exported as runnable actions, while the __init__ parameters are
|
# be exported as runnable actions, while the __init__ parameters are
|
||||||
# configuration attributes that you can initialize in your config.yaml.
|
# 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
|
# 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
|
# for reference. You can easily install the required dependencies for the plugin through
|
||||||
# pip install 'platypush[hue]'
|
# pip install 'platypush[hue]'
|
||||||
light.hue:
|
light.hue:
|
||||||
|
@ -54,14 +54,14 @@ light.hue:
|
||||||
- Living Room
|
- Living Room
|
||||||
|
|
||||||
# Example configuration of music.mpd plugin, see
|
# 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]'
|
# You can easily install the dependencies through pip install 'platypush[mpd]'
|
||||||
music.mpd:
|
music.mpd:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6600
|
port: 6600
|
||||||
|
|
||||||
# Example configuration of media.chromecast plugin, see
|
# 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]'
|
# You can easily install the dependencies through pip install 'platypush[chromecast]'
|
||||||
media.chromecast:
|
media.chromecast:
|
||||||
chromecast: Living Room TV
|
chromecast: Living Room TV
|
||||||
|
@ -69,16 +69,25 @@ media.chromecast:
|
||||||
# Plugins with empty configuration can also be explicitly enabled by specifying
|
# 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
|
# enabled=True or disabled=False (it's a good practice if you want the
|
||||||
# corresponding web panel to be enabled, if available)
|
# corresponding web panel to be enabled, if available)
|
||||||
camera.pi:
|
camera:
|
||||||
enabled: True
|
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
|
# Support for calendars - in this case Google and Facebook calendars
|
||||||
# Installing the dependencies: pip install 'platypush[ical,google]'
|
# Installing the dependencies: pip install 'platypush[ical,google]'
|
||||||
calendar:
|
calendar:
|
||||||
calendars:
|
calendars:
|
||||||
- type: platypush.plugins.google.calendar.GoogleCalendarPlugin
|
-
|
||||||
- type: platypush.plugins.calendar.ical.CalendarIcalPlugin
|
type: platypush.plugins.google.calendar.GoogleCalendarPlugin
|
||||||
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
|
-
|
||||||
|
type: platypush.plugins.calendar.ical.CalendarIcalPlugin
|
||||||
|
url: https://www.facebook.com/events/ical/upcoming/?uid=your_user_id&key=your_key
|
||||||
|
|
||||||
## --
|
## --
|
||||||
## Backends configuration examples
|
## Backends configuration examples
|
||||||
|
@ -88,10 +97,10 @@ calendar:
|
||||||
# to happen and either trigger events or provide additional services on top of platypush.
|
# 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
|
# 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
|
# 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
|
# Moreover, most of the backends will generate events that you can react to through custom
|
||||||
# event hooks. Check here for the events documentation:
|
# 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
|
# You may usually want to enable the HTTP backend, as it provides many useful features on
|
||||||
# top of platypush. Among those:
|
# top of platypush. Among those:
|
||||||
|
@ -109,14 +118,40 @@ calendar:
|
||||||
backend.http:
|
backend.http:
|
||||||
# Listening port
|
# Listening port
|
||||||
port: 8008
|
port: 8008
|
||||||
# Websocket port
|
|
||||||
websocket_port: 8009
|
|
||||||
|
|
||||||
# Through resource_dirs you can specify external folders whose content can be accessed on
|
# 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
|
# 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.
|
# our pictures and we mount it to the '/carousel' endpoint.
|
||||||
resource_dirs:
|
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
|
# 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
|
# 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]"'
|
# Install the required dependencies through 'pip install "platypush[rss,db]"'
|
||||||
backend.http.poll:
|
backend.http.poll:
|
||||||
requests:
|
requests:
|
||||||
- type: platypush.backend.http.request.rss.RssUpdates # HTTP poll type (RSS)
|
-
|
||||||
# Remote URL
|
# HTTP poll type (RSS)
|
||||||
url: http://www.theguardian.com/rss/world
|
type: platypush.backend.http.request.rss.RssUpdates
|
||||||
# Custom title
|
# Remote URL
|
||||||
title: The Guardian - World News
|
url: http://www.theguardian.com/rss/world
|
||||||
# How often we should check for changes
|
# Custom title
|
||||||
poll_seconds: 600
|
title: The Guardian - World News
|
||||||
# Maximum number of new entries to be processed
|
# How often we should check for changes
|
||||||
max_entries: 10
|
poll_seconds: 600
|
||||||
|
# Maximum number of new entries to be processed
|
||||||
- type: platypush.backend.http.request.rss.RssUpdates
|
max_entries: 10
|
||||||
url: http://www.physorg.com/rss-feed
|
-
|
||||||
title: Phys.org
|
type: platypush.backend.http.request.rss.RssUpdates
|
||||||
poll_seconds: 600
|
url: http://www.physorg.com/rss-feed
|
||||||
max_entries: 10
|
title: Phys.org
|
||||||
|
poll_seconds: 600
|
||||||
- type: platypush.backend.http.request.rss.RssUpdates
|
max_entries: 10
|
||||||
url: http://feeds.feedburner.com/Techcrunch
|
-
|
||||||
title: Tech Crunch
|
type: platypush.backend.http.request.rss.RssUpdates
|
||||||
poll_seconds: 600
|
url: http://feeds.feedburner.com/Techcrunch
|
||||||
max_entries: 10
|
title: Tech Crunch
|
||||||
|
poll_seconds: 600
|
||||||
- type: platypush.backend.http.request.rss.RssUpdates
|
max_entries: 10
|
||||||
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
-
|
||||||
title: The New York Times
|
type: platypush.backend.http.request.rss.RssUpdates
|
||||||
poll_seconds: 300
|
url: http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||||
max_entries: 10
|
title: The New York Times
|
||||||
|
poll_seconds: 300
|
||||||
|
max_entries: 10
|
||||||
|
|
||||||
# MQTT backend. Installed required dependencies through 'pip install "platypush[mqtt]"'
|
# MQTT backend. Installed required dependencies through 'pip install "platypush[mqtt]"'
|
||||||
backend.mqtt:
|
backend.mqtt:
|
||||||
|
@ -159,15 +196,15 @@ backend.mqtt:
|
||||||
host: mqtt-server
|
host: mqtt-server
|
||||||
# By default the backend will listen for messages on the platypush_bus_mq/device_id
|
# 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, 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
|
# Raw TCP socket backend. It can run commands sent as JSON over telnet or netcat
|
||||||
#backend.tcp:
|
backend.tcp:
|
||||||
# port: 3333
|
port: 3333
|
||||||
|
|
||||||
# Websocket backend. Install required dependencies through 'pip install "platypush[http]"'
|
# Websocket backend. Install required dependencies through 'pip install "platypush[http]"'
|
||||||
#backend.websocket:
|
backend.websocket:
|
||||||
# port: 8765
|
port: 8765
|
||||||
|
|
||||||
## --
|
## --
|
||||||
## Assistant configuration examples
|
## Assistant configuration examples
|
||||||
|
@ -217,12 +254,9 @@ backend.assistant.snowboy:
|
||||||
assistant.echo:
|
assistant.echo:
|
||||||
audio_player: mplayer
|
audio_player: mplayer
|
||||||
|
|
||||||
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant-legacy]"'
|
# Install Google Assistant dependencies with 'pip install "platypush[google-assistant]"'
|
||||||
assistant.google:
|
assistant.google.pushtotalk:
|
||||||
enabled: True
|
language: en-US
|
||||||
|
|
||||||
backend.assistant.google:
|
|
||||||
enabled: True
|
|
||||||
|
|
||||||
## --
|
## --
|
||||||
## Procedure examples
|
## Procedure examples
|
||||||
|
@ -293,14 +327,14 @@ procedure.outside_home:
|
||||||
procedure.send_request(target, action, args):
|
procedure.send_request(target, action, args):
|
||||||
- action: mqtt.send_message
|
- action: mqtt.send_message
|
||||||
args:
|
args:
|
||||||
topic: platypush_bus_mq/${target}
|
topic: my_platypush_bus/${target}
|
||||||
host: mqtt-server
|
host: mqtt-server
|
||||||
port: 1883
|
port: 1883
|
||||||
msg:
|
msg:
|
||||||
type: request
|
type: request
|
||||||
target: ${target}
|
target: ${target}
|
||||||
action: ${action}
|
action: ${action}
|
||||||
args: ${args}
|
args: "${context.get('args', {}}"
|
||||||
|
|
||||||
## --
|
## --
|
||||||
## Event hook examples
|
## Event hook examples
|
||||||
|
@ -345,8 +379,10 @@ event.hook.SearchSongVoiceCommand:
|
||||||
- action: music.mpd.search
|
- action: music.mpd.search
|
||||||
args:
|
args:
|
||||||
filter:
|
filter:
|
||||||
artist: ${artist}
|
- artist
|
||||||
title: ${title}
|
- ${artist}
|
||||||
|
- any
|
||||||
|
- ${title}
|
||||||
|
|
||||||
# Play the first search result
|
# Play the first search result
|
||||||
- action: music.mpd.play
|
- action: music.mpd.play
|
||||||
|
|
|
@ -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>
|
|
|
@ -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')
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Platypush daemon
|
Description=Platypush daemon
|
||||||
After=network.target redis.service
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
# platypush installation path
|
# platypush installation path
|
||||||
|
|
|
@ -16,17 +16,18 @@ from .context import register_backends
|
||||||
from .cron.scheduler import CronScheduler
|
from .cron.scheduler import CronScheduler
|
||||||
from .event.processor import EventProcessor
|
from .event.processor import EventProcessor
|
||||||
from .logger import Logger
|
from .logger import Logger
|
||||||
from .message.event import Event
|
from .message.event import Event, StopEvent
|
||||||
from .message.event.application import ApplicationStartedEvent
|
from .message.event.application import ApplicationStartedEvent, ApplicationStoppedEvent
|
||||||
from .message.request import Request
|
from .message.request import Request
|
||||||
from .message.response import Response
|
from .message.response import Response
|
||||||
from .utils import set_thread_name
|
from .utils import set_thread_name
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
__author__ = 'Fabio Manganiello <blacklight86@gmail.com>'
|
||||||
__version__ = '0.21.1'
|
__version__ = '0.12.10'
|
||||||
|
|
||||||
logger = logging.getLogger('platypush')
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
LOGGER.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Daemon:
|
class Daemon:
|
||||||
|
@ -41,9 +42,6 @@ class Daemon:
|
||||||
# - plugins will post the responses they process
|
# - plugins will post the responses they process
|
||||||
bus = None
|
bus = None
|
||||||
|
|
||||||
# Default bus queue name
|
|
||||||
_default_redis_queue = 'platypush/bus'
|
|
||||||
|
|
||||||
pidfile = None
|
pidfile = None
|
||||||
|
|
||||||
# backend_name => backend_obj map
|
# backend_name => backend_obj map
|
||||||
|
@ -53,7 +51,7 @@ class Daemon:
|
||||||
n_tries = 2
|
n_tries = 2
|
||||||
|
|
||||||
def __init__(self, config_file=None, pidfile=None, requests_to_process=None,
|
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
|
Constructor
|
||||||
Params:
|
Params:
|
||||||
|
@ -67,7 +65,6 @@ class Daemon:
|
||||||
capture by the logging system
|
capture by the logging system
|
||||||
no_capture_stderr -- Set to true if you want to disable the stderr
|
no_capture_stderr -- Set to true if you want to disable the stderr
|
||||||
capture by the logging system
|
capture by the logging system
|
||||||
redis_queue -- Name of the (Redis) queue used for dispatching messages (default: platypush/bus).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if pidfile:
|
if pidfile:
|
||||||
|
@ -75,21 +72,15 @@ class Daemon:
|
||||||
with open(self.pidfile, 'w') as f:
|
with open(self.pidfile, 'w') as f:
|
||||||
f.write(str(os.getpid()))
|
f.write(str(os.getpid()))
|
||||||
|
|
||||||
self.redis_queue = redis_queue or self._default_redis_queue
|
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
Config.init(self.config_file)
|
Config.init(self.config_file)
|
||||||
logging.basicConfig(**Config.get('logging'))
|
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_stdout = no_capture_stdout
|
||||||
self.no_capture_stderr = no_capture_stderr
|
self.no_capture_stderr = no_capture_stderr
|
||||||
self.event_processor = EventProcessor()
|
self.event_processor = EventProcessor()
|
||||||
self.requests_to_process = requests_to_process
|
self.requests_to_process = requests_to_process
|
||||||
self.processed_requests = 0
|
self.processed_requests = 0
|
||||||
self.cron_scheduler = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build_from_cmdline(cls, args):
|
def build_from_cmdline(cls, args):
|
||||||
|
@ -115,17 +106,11 @@ class Daemon:
|
||||||
help="Set this flag if you have max stack depth " +
|
help="Set this flag if you have max stack depth " +
|
||||||
"exceeded errors so stderr won't be captured by " +
|
"exceeded errors so stderr won't be captured by " +
|
||||||
"the logging system")
|
"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)
|
opts, args = parser.parse_known_args(args)
|
||||||
return cls(config_file=opts.config, pidfile=opts.pidfile,
|
return cls(config_file=opts.config, pidfile=opts.pidfile,
|
||||||
no_capture_stdout=opts.no_capture_stdout,
|
no_capture_stdout=opts.no_capture_stdout,
|
||||||
no_capture_stderr=opts.no_capture_stderr,
|
no_capture_stderr=opts.no_capture_stderr)
|
||||||
redis_queue=opts.redis_queue)
|
|
||||||
|
|
||||||
def on_message(self):
|
def on_message(self):
|
||||||
"""
|
"""
|
||||||
|
@ -143,17 +128,20 @@ class Daemon:
|
||||||
try:
|
try:
|
||||||
msg.execute(n_tries=self.n_tries)
|
msg.execute(n_tries=self.n_tries)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
logger.info('Dropped unauthorized request: {}'.format(msg))
|
LOGGER.info('Dropped unauthorized request: {}'.format(msg))
|
||||||
|
|
||||||
self.processed_requests += 1
|
self.processed_requests += 1
|
||||||
if self.requests_to_process \
|
if self.requests_to_process \
|
||||||
and self.processed_requests >= self.requests_to_process:
|
and self.processed_requests >= self.requests_to_process:
|
||||||
self.stop_app()
|
self.stop_app()
|
||||||
elif isinstance(msg, Response):
|
elif isinstance(msg, Response):
|
||||||
logger.info('Received response: {}'.format(msg))
|
LOGGER.info('Received response: {}'.format(msg))
|
||||||
|
elif isinstance(msg, StopEvent) and msg.targets_me():
|
||||||
|
LOGGER.info('Received STOP event: {}'.format(msg))
|
||||||
|
self.stop_app()
|
||||||
elif isinstance(msg, Event):
|
elif isinstance(msg, Event):
|
||||||
if not msg.disable_logging:
|
if not msg.disable_logging:
|
||||||
logger.info('Received event: {}'.format(msg))
|
LOGGER.info('Received event: {}'.format(msg))
|
||||||
self.event_processor.process_event(msg)
|
self.event_processor.process_event(msg)
|
||||||
|
|
||||||
return _f
|
return _f
|
||||||
|
@ -162,20 +150,22 @@ class Daemon:
|
||||||
""" Stops the backends and the bus """
|
""" Stops the backends and the bus """
|
||||||
for backend in self.backends.values():
|
for backend in self.backends.values():
|
||||||
backend.stop()
|
backend.stop()
|
||||||
|
|
||||||
self.bus.stop()
|
self.bus.stop()
|
||||||
if self.cron_scheduler:
|
|
||||||
self.cron_scheduler.stop()
|
|
||||||
|
|
||||||
def run(self):
|
def start(self):
|
||||||
""" Start the daemon """
|
""" Start the daemon """
|
||||||
if not self.no_capture_stdout:
|
if not self.no_capture_stdout:
|
||||||
sys.stdout = Logger(logger.info)
|
sys.stdout = Logger(LOGGER.info)
|
||||||
if not self.no_capture_stderr:
|
if not self.no_capture_stderr:
|
||||||
sys.stderr = Logger(logger.warning)
|
sys.stderr = Logger(LOGGER.warning)
|
||||||
|
|
||||||
set_thread_name('platypush')
|
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
|
# Initialize the backends and link them to the bus
|
||||||
self.backends = register_backends(bus=self.bus, global_scope=True)
|
self.backends = register_backends(bus=self.bus, global_scope=True)
|
||||||
|
@ -186,8 +176,7 @@ class Daemon:
|
||||||
|
|
||||||
# Start the cron scheduler
|
# Start the cron scheduler
|
||||||
if Config.get_cronjobs():
|
if Config.get_cronjobs():
|
||||||
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())
|
CronScheduler(jobs=Config.get_cronjobs()).start()
|
||||||
self.cron_scheduler.start()
|
|
||||||
|
|
||||||
self.bus.post(ApplicationStartedEvent())
|
self.bus.post(ApplicationStartedEvent())
|
||||||
|
|
||||||
|
@ -195,8 +184,9 @@ class Daemon:
|
||||||
try:
|
try:
|
||||||
self.bus.poll()
|
self.bus.poll()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, terminating application')
|
LOGGER.info('SIGINT received, terminating application')
|
||||||
finally:
|
finally:
|
||||||
|
self.bus.post(ApplicationStoppedEvent())
|
||||||
self.stop_app()
|
self.stop_app()
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,8 +194,9 @@ def main():
|
||||||
"""
|
"""
|
||||||
Platypush daemon main
|
Platypush daemon main
|
||||||
"""
|
"""
|
||||||
|
|
||||||
app = Daemon.build_from_cmdline(sys.argv[1:])
|
app = Daemon.build_from_cmdline(sys.argv[1:])
|
||||||
app.run()
|
app.start()
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -4,24 +4,21 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import threading
|
||||||
import socket
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from threading import Thread, Event as ThreadEvent, get_ident
|
from threading import Thread
|
||||||
from typing import Optional, Dict
|
from typing import Optional
|
||||||
|
|
||||||
from platypush.bus import Bus
|
from platypush.bus import Bus
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.context import get_backend
|
from platypush.context import get_backend
|
||||||
from platypush.message.event.zeroconf import ZeroconfServiceAddedEvent, ZeroconfServiceRemovedEvent
|
|
||||||
from platypush.utils import set_timeout, clear_timeout, \
|
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.event import EventGenerator
|
||||||
from platypush.message import Message
|
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.request import Request
|
||||||
from platypush.message.response import Response
|
from platypush.message.response import Response
|
||||||
|
|
||||||
|
@ -53,7 +50,7 @@ class Backend(Thread, EventGenerator):
|
||||||
|
|
||||||
self._thread_name = self.__class__.__name__
|
self._thread_name = self.__class__.__name__
|
||||||
EventGenerator.__init__(self)
|
EventGenerator.__init__(self)
|
||||||
Thread.__init__(self, name=self._thread_name, daemon=True)
|
Thread.__init__(self, name=self._thread_name)
|
||||||
|
|
||||||
# If no bus is specified, create an internal queue where
|
# If no bus is specified, create an internal queue where
|
||||||
# the received messages will be pushed
|
# the received messages will be pushed
|
||||||
|
@ -61,11 +58,10 @@ class Backend(Thread, EventGenerator):
|
||||||
self.poll_seconds = float(poll_seconds) if poll_seconds else None
|
self.poll_seconds = float(poll_seconds) if poll_seconds else None
|
||||||
self.device_id = Config.get('device_id')
|
self.device_id = Config.get('device_id')
|
||||||
self.thread_id = None
|
self.thread_id = None
|
||||||
self._stop_event = ThreadEvent()
|
self._should_stop = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
self._kwargs = kwargs
|
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
|
|
||||||
|
|
||||||
# Internal-only, we set the request context on a backend if that
|
# Internal-only, we set the request context on a backend if that
|
||||||
# backend is intended to react for a response to a specific request
|
# backend is intended to react for a response to a specific request
|
||||||
|
@ -75,6 +71,8 @@ class Backend(Thread, EventGenerator):
|
||||||
if 'logging' in kwargs:
|
if 'logging' in kwargs:
|
||||||
self.logger.setLevel(getattr(logging, kwargs.get('logging').upper()))
|
self.logger.setLevel(getattr(logging, kwargs.get('logging').upper()))
|
||||||
|
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
def on_message(self, msg):
|
def on_message(self, msg):
|
||||||
"""
|
"""
|
||||||
Callback when a message is received on the backend.
|
Callback when a message is received on the backend.
|
||||||
|
@ -101,8 +99,12 @@ class Backend(Thread, EventGenerator):
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
msg.backend = self # Augment message to be able to process responses
|
if isinstance(msg, StopEvent) and msg.targets_me():
|
||||||
self.bus.post(msg)
|
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):
|
def _is_expected_response(self, msg):
|
||||||
""" Internal only - returns true if we are expecting for a response
|
""" Internal only - returns true if we are expecting for a response
|
||||||
|
@ -219,7 +221,7 @@ class Backend(Thread, EventGenerator):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
""" Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """
|
""" 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)
|
set_thread_name(self._thread_name)
|
||||||
if not callable(self.loop):
|
if not callable(self.loop):
|
||||||
return
|
return
|
||||||
|
@ -262,17 +264,20 @@ class Backend(Thread, EventGenerator):
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stops the backend thread by sending a STOP event on its bus """
|
""" Stops the backend thread by sending a STOP event on its bus """
|
||||||
def _async_stop():
|
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._stop_event.set()
|
||||||
self.unregister_service()
|
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
|
|
||||||
Thread(target=_async_stop).start()
|
Thread(target=_async_stop).start()
|
||||||
|
|
||||||
def should_stop(self):
|
def should_stop(self):
|
||||||
return self._stop_event.is_set()
|
return self._should_stop
|
||||||
|
|
||||||
def wait_stop(self, timeout=None) -> bool:
|
def wait_stop(self, timeout=None):
|
||||||
return self._stop_event.wait(timeout)
|
self._stop_event.wait(timeout)
|
||||||
|
|
||||||
def _get_redis(self):
|
def _get_redis(self):
|
||||||
import redis
|
import redis
|
||||||
|
@ -301,104 +306,5 @@ class Backend(Thread, EventGenerator):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error('Error while processing response to {}: {}'.format(msg, str(e)))
|
self.logger.error('Error while processing response to {}: {}'.format(msg, str(e)))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_ip() -> str:
|
|
||||||
"""
|
|
||||||
Get the IP address of the machine.
|
|
||||||
"""
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.connect(('8.8.8.8', 80))
|
|
||||||
addr = s.getsockname()[0]
|
|
||||||
s.close()
|
|
||||||
return addr
|
|
||||||
|
|
||||||
def register_service(self,
|
|
||||||
port: Optional[int] = None,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
srv_type: Optional[str] = None,
|
|
||||||
srv_name: Optional[str] = None,
|
|
||||||
udp: bool = False,
|
|
||||||
properties: Optional[Dict] = None):
|
|
||||||
"""
|
|
||||||
Initialize the Zeroconf service configuration for this backend.
|
|
||||||
|
|
||||||
:param port: Service listen port (default: the backend ``port`` attribute if available, or ``None``).
|
|
||||||
:param name: Service short name (default: backend name).
|
|
||||||
:param srv_type: Service type (default: ``_platypush-{name}._{proto}.local.``).
|
|
||||||
:param srv_name: Full service name (default: ``{hostname or device_id}.{type}``).
|
|
||||||
:param udp: Set to True if this is a UDP service.
|
|
||||||
:param properties: Extra properties to be passed on the service. Default:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Platypush",
|
|
||||||
"vendor": "Platypush",
|
|
||||||
"version": "{platypush_version}"
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from zeroconf import ServiceInfo, Zeroconf
|
|
||||||
from platypush.plugins.zeroconf import ZeroconfListener
|
|
||||||
except ImportError:
|
|
||||||
self.logger.warning('zeroconf package not available, service discovery will be disabled.')
|
|
||||||
return
|
|
||||||
|
|
||||||
self.zeroconf = Zeroconf()
|
|
||||||
srv_desc = {
|
|
||||||
'name': 'Platypush',
|
|
||||||
'vendor': 'Platypush',
|
|
||||||
'version': __version__,
|
|
||||||
**(properties or {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
|
|
||||||
srv_type = srv_type or '_platypush-{name}._{proto}.local.'.format(name=name, proto='udp' if udp else 'tcp')
|
|
||||||
srv_name = srv_name or '{host}.{type}'.format(host=self.device_id, type=srv_type)
|
|
||||||
|
|
||||||
if port:
|
|
||||||
srv_port = port
|
|
||||||
else:
|
|
||||||
srv_port = self.port if hasattr(self, 'port') else None
|
|
||||||
|
|
||||||
self.zeroconf_info = ServiceInfo(srv_type, srv_name,
|
|
||||||
addresses=[socket.inet_aton(self._get_ip())],
|
|
||||||
port=srv_port,
|
|
||||||
weight=0,
|
|
||||||
priority=0,
|
|
||||||
properties=srv_desc)
|
|
||||||
|
|
||||||
if not self.zeroconf_info:
|
|
||||||
self.logger.warning('Could not register Zeroconf service')
|
|
||||||
return
|
|
||||||
|
|
||||||
self.zeroconf.register_service(self.zeroconf_info)
|
|
||||||
self.bus.post(ZeroconfServiceAddedEvent(service_type=srv_type, service_name=srv_name,
|
|
||||||
service_info=ZeroconfListener.parse_service_info(self.zeroconf_info)))
|
|
||||||
|
|
||||||
def unregister_service(self):
|
|
||||||
"""
|
|
||||||
Unregister the Zeroconf service configuration if available.
|
|
||||||
"""
|
|
||||||
if self.zeroconf and self.zeroconf_info:
|
|
||||||
try:
|
|
||||||
self.zeroconf.unregister_service(self.zeroconf_info)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.warning('Could not register Zeroconf service {}: {}: {}'.format(
|
|
||||||
self.zeroconf_info.name, type(e).__name__, str(e)))
|
|
||||||
|
|
||||||
if self.zeroconf:
|
|
||||||
self.zeroconf.close()
|
|
||||||
|
|
||||||
if self.zeroconf_info:
|
|
||||||
self.bus.post(ZeroconfServiceRemovedEvent(service_type=self.zeroconf_info.type,
|
|
||||||
service_name=self.zeroconf_info.name))
|
|
||||||
else:
|
|
||||||
self.bus.post(ZeroconfServiceRemovedEvent(service_type=None, service_name=None))
|
|
||||||
|
|
||||||
self.zeroconf_info = None
|
|
||||||
self.zeroconf = None
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -42,7 +42,6 @@ class AdafruitIoBackend(Backend):
|
||||||
if not plugin:
|
if not plugin:
|
||||||
raise RuntimeError('Adafruit IO plugin not configured')
|
raise RuntimeError('Adafruit IO plugin not configured')
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
self._client = MQTTClient(plugin._username, plugin._key)
|
self._client = MQTTClient(plugin._username, plugin._key)
|
||||||
self._client.on_connect = self.on_connect()
|
self._client.on_connect = self.on_connect()
|
||||||
self._client.on_disconnect = self.on_disconnect()
|
self._client.on_disconnect = self.on_disconnect()
|
||||||
|
@ -53,25 +52,18 @@ class AdafruitIoBackend(Backend):
|
||||||
for feed in self.feeds:
|
for feed in self.feeds:
|
||||||
client.subscribe(feed)
|
client.subscribe(feed)
|
||||||
self.bus.post(ConnectedEvent())
|
self.bus.post(ConnectedEvent())
|
||||||
|
|
||||||
return _handler
|
return _handler
|
||||||
|
|
||||||
def on_disconnect(self):
|
def on_disconnect(self):
|
||||||
def _handler(client):
|
def _handler(client):
|
||||||
self.bus.post(DisconnectedEvent())
|
self.bus.post(DisconnectedEvent())
|
||||||
|
|
||||||
return _handler
|
return _handler
|
||||||
|
|
||||||
def on_message(self, msg):
|
def on_message(self):
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
def _handler(client, feed, data):
|
def _handler(client, feed, data):
|
||||||
try:
|
try: data = float(data)
|
||||||
data = float(data)
|
except: pass
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug('Not a number: {}: {}'.format(data, e))
|
|
||||||
|
|
||||||
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
|
self.bus.post(FeedUpdateEvent(feed=feed, data=data))
|
||||||
|
|
||||||
return _handler
|
return _handler
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
@ -89,4 +81,5 @@ class AdafruitIoBackend(Backend):
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import datetime
|
import datetime
|
||||||
import enum
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
@ -7,7 +6,7 @@ import threading
|
||||||
from typing import Optional, Union, Dict, Any, List
|
from typing import Optional, Union, Dict, Any, List
|
||||||
|
|
||||||
import croniter
|
import croniter
|
||||||
from dateutil.tz import gettz
|
import enum
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_bus, get_plugin
|
from platypush.context import get_bus, get_plugin
|
||||||
|
@ -55,20 +54,18 @@ class Alarm:
|
||||||
self._runtime_snooze_interval = snooze_interval
|
self._runtime_snooze_interval = snooze_interval
|
||||||
|
|
||||||
def get_next(self) -> float:
|
def get_next(self) -> float:
|
||||||
now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
now = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cron = croniter.croniter(self.when, now)
|
cron = croniter.croniter(self.when, now)
|
||||||
return cron.get_next()
|
return cron.get_next()
|
||||||
except (AttributeError, croniter.CroniterBadCronError):
|
except (AttributeError, croniter.CroniterBadCronError):
|
||||||
try:
|
try:
|
||||||
timestamp = datetime.datetime.fromisoformat(self.when).replace(
|
timestamp = datetime.datetime.fromisoformat(self.when).timestamp()
|
||||||
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
|
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable]
|
timestamp = (datetime.datetime.now() + datetime.timedelta(seconds=int(self.when))).timestamp()
|
||||||
datetime.timedelta(seconds=int(self.when)))
|
|
||||||
|
|
||||||
return timestamp.timestamp() if timestamp >= now else None
|
return timestamp if timestamp >= now else None
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return self._enabled
|
return self._enabled
|
||||||
|
|
|
@ -21,10 +21,12 @@ class AssistantGoogleBackend(AssistantBackend):
|
||||||
|
|
||||||
It listens for voice commands and post conversation events on the bus.
|
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:
|
**WARNING**: This backend is deprecated, as the underlying Google Assistant
|
||||||
https://developers.google.com/assistant/sdk/reference/library/python/. This backend still works on most of the
|
library has been deprecated too: https://developers.google.com/assistant/sdk/reference/library/python/
|
||||||
devices where I use it, but its correct functioning is not guaranteed as the assistant library is no longer
|
The old library might still work on some systems but its proper functioning
|
||||||
maintained.
|
is not guaranteed.
|
||||||
|
Please use the Snowboy backend for hotword detection and the Google Assistant
|
||||||
|
push-to-talk plugin for assistant interaction instead.
|
||||||
|
|
||||||
Triggers:
|
Triggers:
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,6 @@ class AssistantSnowboyBackend(AssistantBackend):
|
||||||
return callback
|
return callback
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
super().on_stop()
|
|
||||||
if self.detector:
|
if self.detector:
|
||||||
self.detector.terminate()
|
self.detector.terminate()
|
||||||
self.detector = None
|
self.detector = None
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import time
|
from typing import Dict, Optional
|
||||||
from threading import Thread, RLock
|
|
||||||
from typing import Dict, Optional, List
|
|
||||||
|
|
||||||
from platypush.backend.sensor import SensorBackend
|
from platypush.backend.sensor import SensorBackend
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
|
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,
|
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10, **kwargs):
|
||||||
track_devices: Optional[List[str]] = None, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
|
: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 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={
|
super().__init__(plugin='bluetooth', plugin_args={
|
||||||
'device_id': device_id,
|
'device_id': device_id,
|
||||||
|
@ -36,72 +31,17 @@ class BluetoothScannerBackend(SensorBackend):
|
||||||
}, **kwargs)
|
}, **kwargs)
|
||||||
|
|
||||||
self._last_seen_devices = {}
|
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]):
|
def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]):
|
||||||
for addr, dev in data.items():
|
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():
|
for addr, dev in self._last_seen_devices.copy().items():
|
||||||
if addr not in data and addr not in self.track_devices:
|
if addr not in data:
|
||||||
self._remove_last_seen_device(addr)
|
self.bus.post(BluetoothDeviceLostEvent(address=dev.pop('addr'), **dev))
|
||||||
|
del self._last_seen_devices[addr]
|
||||||
def run(self):
|
|
||||||
self._tracking_thread = Thread(target=self._bt_tracker)
|
|
||||||
self._tracking_thread.start()
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
super().on_stop()
|
|
||||||
if self._tracking_thread and self._tracking_thread.is_alive():
|
|
||||||
self.logger.info('Waiting for the Bluetooth tracking thread to stop')
|
|
||||||
self._tracking_thread.join(timeout=self.scan_duration)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -13,41 +13,37 @@ Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
|
||||||
import asyncio
|
import asyncio
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
import time
|
||||||
import struct
|
import struct
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
class CreateConnectionChannelError(Enum):
|
class CreateConnectionChannelError(Enum):
|
||||||
NoError = 0
|
NoError = 0
|
||||||
MaxPendingConnectionsReached = 1
|
MaxPendingConnectionsReached = 1
|
||||||
|
|
||||||
|
|
||||||
class ConnectionStatus(Enum):
|
class ConnectionStatus(Enum):
|
||||||
Disconnected = 0
|
Disconnected = 0
|
||||||
Connected = 1
|
Connected = 1
|
||||||
Ready = 2
|
Ready = 2
|
||||||
|
|
||||||
|
|
||||||
class DisconnectReason(Enum):
|
class DisconnectReason(Enum):
|
||||||
Unspecified = 0
|
Unspecified = 0
|
||||||
ConnectionEstablishmentFailed = 1
|
ConnectionEstablishmentFailed = 1
|
||||||
TimedOut = 2
|
TimedOut = 2
|
||||||
BondingKeysMismatch = 3
|
BondingKeysMismatch = 3
|
||||||
|
|
||||||
|
|
||||||
class RemovedReason(Enum):
|
class RemovedReason(Enum):
|
||||||
RemovedByThisClient = 0
|
RemovedByThisClient = 0
|
||||||
ForceDisconnectedByThisClient = 1
|
ForceDisconnectedByThisClient = 1
|
||||||
ForceDisconnectedByOtherClient = 2
|
ForceDisconnectedByOtherClient = 2
|
||||||
|
|
||||||
ButtonIsPrivate = 3
|
ButtonIsPrivate = 3
|
||||||
VerifyTimeout = 4
|
VerifyTimeout = 4
|
||||||
InternetBackendError = 5
|
InternetBackendError = 5
|
||||||
InvalidData = 6
|
InvalidData = 6
|
||||||
|
|
||||||
CouldntLoadDevice = 7
|
CouldntLoadDevice = 7
|
||||||
|
|
||||||
|
|
||||||
class ClickType(Enum):
|
class ClickType(Enum):
|
||||||
ButtonDown = 0
|
ButtonDown = 0
|
||||||
ButtonUp = 1
|
ButtonUp = 1
|
||||||
|
@ -56,24 +52,20 @@ class ClickType(Enum):
|
||||||
ButtonDoubleClick = 4
|
ButtonDoubleClick = 4
|
||||||
ButtonHold = 5
|
ButtonHold = 5
|
||||||
|
|
||||||
|
|
||||||
class BdAddrType(Enum):
|
class BdAddrType(Enum):
|
||||||
PublicBdAddrType = 0
|
PublicBdAddrType = 0
|
||||||
RandomBdAddrType = 1
|
RandomBdAddrType = 1
|
||||||
|
|
||||||
|
|
||||||
class LatencyMode(Enum):
|
class LatencyMode(Enum):
|
||||||
NormalLatency = 0
|
NormalLatency = 0
|
||||||
LowLatency = 1
|
LowLatency = 1
|
||||||
HighLatency = 2
|
HighLatency = 2
|
||||||
|
|
||||||
|
|
||||||
class BluetoothControllerState(Enum):
|
class BluetoothControllerState(Enum):
|
||||||
Detached = 0
|
Detached = 0
|
||||||
Resetting = 1
|
Resetting = 1
|
||||||
Attached = 2
|
Attached = 2
|
||||||
|
|
||||||
|
|
||||||
class ScanWizardResult(Enum):
|
class ScanWizardResult(Enum):
|
||||||
WizardSuccess = 0
|
WizardSuccess = 0
|
||||||
WizardCancelledByUser = 1
|
WizardCancelledByUser = 1
|
||||||
|
@ -83,26 +75,24 @@ class ScanWizardResult(Enum):
|
||||||
WizardInternetBackendError = 5
|
WizardInternetBackendError = 5
|
||||||
WizardInvalidData = 6
|
WizardInvalidData = 6
|
||||||
|
|
||||||
|
|
||||||
class ButtonScanner:
|
class ButtonScanner:
|
||||||
"""ButtonScanner class.
|
"""ButtonScanner class.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
scanner = ButtonScanner()
|
scanner = ButtonScanner()
|
||||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
||||||
client.add_scanner(scanner)
|
client.add_scanner(scanner)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_id = next(ButtonScanner._cnt)
|
self._scan_id = next(ButtonScanner._cnt)
|
||||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
||||||
|
|
||||||
|
|
||||||
class ScanWizard:
|
class ScanWizard:
|
||||||
"""ScanWizard class
|
"""ScanWizard class
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
wizard = ScanWizard()
|
wizard = ScanWizard()
|
||||||
wizard.on_found_private_button = lambda scan_wizard: ...
|
wizard.on_found_private_button = lambda scan_wizard: ...
|
||||||
|
@ -111,9 +101,9 @@ class ScanWizard:
|
||||||
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
||||||
client.add_scan_wizard(wizard)
|
client.add_scan_wizard(wizard)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_wizard_id = next(ScanWizard._cnt)
|
self._scan_wizard_id = next(ScanWizard._cnt)
|
||||||
self._bd_addr = None
|
self._bd_addr = None
|
||||||
|
@ -123,34 +113,33 @@ class ScanWizard:
|
||||||
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
|
self.on_button_connected = lambda scan_wizard, bd_addr, name: None
|
||||||
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
|
self.on_completed = lambda scan_wizard, result, bd_addr, name: None
|
||||||
|
|
||||||
|
|
||||||
class ButtonConnectionChannel:
|
class ButtonConnectionChannel:
|
||||||
"""ButtonConnectionChannel class.
|
"""ButtonConnectionChannel class.
|
||||||
|
|
||||||
This class represents a connection channel to a Flic button.
|
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).
|
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.
|
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
|
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,
|
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
|
||||||
referencing this object.
|
referencing this object.
|
||||||
|
|
||||||
Available properties and the function parameters are:
|
Available properties and the function parameters are:
|
||||||
on_create_connection_channel_response: channel, error, connection_status
|
on_create_connection_channel_response: channel, error, connection_status
|
||||||
on_removed: channel, removed_reason
|
on_removed: channel, removed_reason
|
||||||
on_connection_status_changed: channel, connection_status, disconnect_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
|
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()
|
_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._conn_id = next(ButtonConnectionChannel._cnt)
|
||||||
self._bd_addr = bd_addr
|
self._bd_addr = bd_addr
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
||||||
self.on_removed = lambda channel, removed_reason: None
|
self.on_removed = lambda channel, removed_reason: None
|
||||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_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_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 = 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
|
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bd_addr(self):
|
def bd_addr(self):
|
||||||
return self._bd_addr
|
return self._bd_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latency_mode(self):
|
def latency_mode(self):
|
||||||
return self._latency_mode
|
return self._latency_mode
|
||||||
|
|
||||||
@latency_mode.setter
|
@latency_mode.setter
|
||||||
def latency_mode(self, latency_mode):
|
def latency_mode(self, latency_mode):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
return
|
return
|
||||||
|
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
self._client._send_command("CmdChangeModeParameters",
|
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
|
||||||
"auto_disconnect_time": self._auto_disconnect_time})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_disconnect_time(self):
|
def auto_disconnect_time(self):
|
||||||
return self._auto_disconnect_time
|
return self._auto_disconnect_time
|
||||||
|
|
||||||
@auto_disconnect_time.setter
|
@auto_disconnect_time.setter
|
||||||
def auto_disconnect_time(self, auto_disconnect_time):
|
def auto_disconnect_time(self, auto_disconnect_time):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
return
|
return
|
||||||
|
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
self._client._send_command("CmdChangeModeParameters",
|
self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
|
||||||
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
|
|
||||||
"auto_disconnect_time": self._auto_disconnect_time})
|
|
||||||
|
|
||||||
|
|
||||||
class FlicClient(asyncio.Protocol):
|
class FlicClient(asyncio.Protocol):
|
||||||
"""FlicClient class.
|
"""FlicClient class.
|
||||||
|
|
||||||
When this class is constructed, a socket connection is established.
|
When this class is constructed, a socket connection is established.
|
||||||
You may then send commands to the server and set timers.
|
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.
|
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.
|
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 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.
|
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 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.
|
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):
|
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_new_verified_button: bd_addr
|
||||||
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_bluetooth_controller_state_change: state
|
on_bluetooth_controller_state_change: state
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_EVENTS = [
|
_EVENTS = [
|
||||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
||||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
("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"),
|
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||||
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
|
||||||
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
("EvtNewVerifiedButton", "<6s", "bd_addr"),
|
||||||
("EvtGetInfoResponse", "<B6sBBhBBH",
|
("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"),
|
||||||
"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"),
|
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||||
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
|
||||||
("EvtBluetoothControllerStateChange", "<B", "state"),
|
("EvtBluetoothControllerStateChange", "<B", "state"),
|
||||||
|
@ -240,9 +223,9 @@ class FlicClient(asyncio.Protocol):
|
||||||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
||||||
]
|
]
|
||||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _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 is None else namedtuple(x[0], x[2]), _EVENTS))
|
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
|
||||||
|
|
||||||
_COMMANDS = [
|
_COMMANDS = [
|
||||||
("CmdGetInfo", "", ""),
|
("CmdGetInfo", "", ""),
|
||||||
("CmdCreateScanner", "<I", "scan_id"),
|
("CmdCreateScanner", "<I", "scan_id"),
|
||||||
|
@ -256,169 +239,168 @@ class FlicClient(asyncio.Protocol):
|
||||||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
||||||
]
|
]
|
||||||
|
|
||||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
_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_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))
|
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||||
|
|
||||||
def __init__(self, loop, parent=None):
|
def __init__(self, loop,parent=None):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.buffer = b""
|
self.buffer=b""
|
||||||
self.transport = None
|
self.transport=None
|
||||||
self.parent = parent
|
self.parent=parent
|
||||||
self._scanners = {}
|
self._scanners = {}
|
||||||
self._scan_wizards = {}
|
self._scan_wizards = {}
|
||||||
self._connection_channels = {}
|
self._connection_channels = {}
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
self.on_new_verified_button = lambda bd_addr: None
|
self.on_new_verified_button = lambda bd_addr: None
|
||||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: 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_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_info = lambda items: None
|
||||||
self.on_get_button_uuid = lambda addr, uuid: None
|
self.on_get_button_uuid = lambda addr, uuid: None
|
||||||
|
|
||||||
def connection_made(self, transport):
|
def connection_made(self, transport):
|
||||||
self.transport = transport
|
self.transport=transport
|
||||||
if self.parent:
|
if self.parent:
|
||||||
self.parent.register_protocol(self)
|
self.parent.register_protocol(self)
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closes the client. The handle_events() method will return."""
|
"""Closes the client. The handle_events() method will return."""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def add_scanner(self, scanner):
|
def add_scanner(self, scanner):
|
||||||
"""Add a ButtonScanner object.
|
"""Add a ButtonScanner object.
|
||||||
|
|
||||||
The scan will start directly once the scanner is added.
|
The scan will start directly once the scanner is added.
|
||||||
"""
|
"""
|
||||||
if scanner._scan_id in self._scanners:
|
if scanner._scan_id in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scanners[scanner._scan_id] = scanner
|
self._scanners[scanner._scan_id] = scanner
|
||||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def remove_scanner(self, scanner):
|
def remove_scanner(self, scanner):
|
||||||
"""Remove a ButtonScanner object.
|
"""Remove a ButtonScanner object.
|
||||||
|
|
||||||
You will no longer receive advertisement packets.
|
You will no longer receive advertisement packets.
|
||||||
"""
|
"""
|
||||||
if scanner._scan_id not in self._scanners:
|
if scanner._scan_id not in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
del self._scanners[scanner._scan_id]
|
del self._scanners[scanner._scan_id]
|
||||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def add_scan_wizard(self, scan_wizard):
|
def add_scan_wizard(self, scan_wizard):
|
||||||
"""Add a ScanWizard object.
|
"""Add a ScanWizard object.
|
||||||
|
|
||||||
The scan wizard will start directly once the scan wizard is added.
|
The scan wizard will start directly once the scan wizard is added.
|
||||||
"""
|
"""
|
||||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def cancel_scan_wizard(self, scan_wizard):
|
def cancel_scan_wizard(self, scan_wizard):
|
||||||
"""Cancel a ScanWizard.
|
"""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.
|
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 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:
|
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def add_connection_channel(self, channel):
|
def add_connection_channel(self, channel):
|
||||||
"""Adds a connection channel to a specific Flic button.
|
"""Adds a connection channel to a specific Flic button.
|
||||||
|
|
||||||
This will start listening for a specific Flic button's connection and button events.
|
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.
|
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
|
The on_create_connection_channel_response callback property will be called on the
|
||||||
connection channel after this command has been received by the server.
|
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.
|
You may have as many connection channels as you wish for a specific Flic Button.
|
||||||
"""
|
"""
|
||||||
if channel._conn_id in self._connection_channels:
|
if channel._conn_id in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
channel._client = self
|
channel._client = self
|
||||||
|
|
||||||
self._connection_channels[channel._conn_id] = channel
|
self._connection_channels[channel._conn_id] = channel
|
||||||
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr,
|
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})
|
||||||
"latency_mode": channel._latency_mode,
|
|
||||||
"auto_disconnect_time": channel._auto_disconnect_time})
|
|
||||||
|
|
||||||
def remove_connection_channel(self, channel):
|
def remove_connection_channel(self, channel):
|
||||||
"""Remove a connection channel.
|
"""Remove a connection channel.
|
||||||
|
|
||||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
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.
|
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:
|
if channel._conn_id not in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
||||||
|
|
||||||
def force_disconnect(self, bd_addr):
|
def force_disconnect(self, bd_addr):
|
||||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
"""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.
|
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})
|
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
"""Get info about the current state of the server.
|
"""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 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:
|
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,
|
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).
|
current_pending_connections, currently_no_space_for_new_connection, bd_addr_of_verified_buttons (a list of bd addresses).
|
||||||
"""
|
"""
|
||||||
self._send_command("CmdGetInfo", {})
|
self._send_command("CmdGetInfo", {})
|
||||||
|
|
||||||
def get_button_uuid(self, bd_addr):
|
def get_button_uuid(self, bd_addr):
|
||||||
"""Get button uuid for a verified button.
|
"""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.
|
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.
|
Responses will arrive in the same order as requested.
|
||||||
|
|
||||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
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.
|
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})
|
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
|
|
||||||
def run_on_handle_events_thread(self, callback):
|
def run_on_handle_events_thread(self, callback):
|
||||||
"""Run a function on the thread that handles the events."""
|
"""Run a function on the thread that handles the events."""
|
||||||
if threading.get_ident() == self._handle_event_thread_ident:
|
if threading.get_ident() == self._handle_event_thread_ident:
|
||||||
callback()
|
callback()
|
||||||
else:
|
else:
|
||||||
self.set_timer(0, callback)
|
self.set_timer(0, callback)
|
||||||
|
|
||||||
def _send_command(self, name, items):
|
def _send_command(self, name, items):
|
||||||
|
|
||||||
for key, value in items.items():
|
for key, value in items.items():
|
||||||
if isinstance(value, Enum):
|
if isinstance(value, Enum):
|
||||||
items[key] = value.value
|
items[key] = value.value
|
||||||
|
|
||||||
if "bd_addr" in items:
|
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]
|
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
||||||
bytes = bytearray(3)
|
bytes = bytearray(3)
|
||||||
|
@ -427,85 +409,83 @@ class FlicClient(asyncio.Protocol):
|
||||||
bytes[2] = opcode
|
bytes[2] = opcode
|
||||||
bytes += data_bytes
|
bytes += data_bytes
|
||||||
self.transport.write(bytes)
|
self.transport.write(bytes)
|
||||||
|
|
||||||
def _dispatch_event(self, data):
|
def _dispatch_event(self, data):
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
opcode = data[0]
|
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
|
return
|
||||||
|
|
||||||
event_name = FlicClient._EVENTS[opcode][0]
|
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()
|
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||||
|
|
||||||
# Process some kind of items whose data type is not supported by struct
|
# Process some kind of items whose data type is not supported by struct
|
||||||
if "bd_addr" in items:
|
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:
|
if "name" in items:
|
||||||
items["name"] = items["name"].decode("utf-8")
|
items["name"] = items["name"].decode("utf-8")
|
||||||
|
|
||||||
if event_name == "EvtCreateConnectionChannelResponse":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
items["error"] = CreateConnectionChannelError(items["error"])
|
items["error"] = CreateConnectionChannelError(items["error"])
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||||
|
|
||||||
if event_name.startswith("EvtButton"):
|
if event_name.startswith("EvtButton"):
|
||||||
items["click_type"] = ClickType(items["click_type"])
|
items["click_type"] = ClickType(items["click_type"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
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["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||||
items["bd_addr_of_verified_buttons"] = []
|
items["bd_addr_of_verified_buttons"] = []
|
||||||
|
|
||||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||||
for i in range(items["nb_verified_buttons"]):
|
for i in range(items["nb_verified_buttons"]):
|
||||||
items["bd_addr_of_verified_buttons"].append(
|
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
||||||
FlicClient._bdaddr_bytes_to_string())
|
|
||||||
pos += 6
|
pos += 6
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
items["state"] = BluetoothControllerState(items["state"])
|
items["state"] = BluetoothControllerState(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||||
if items["uuid"] == "00000000000000000000000000000000":
|
if items["uuid"] == "00000000000000000000000000000000":
|
||||||
items["uuid"] = None
|
items["uuid"] = None
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
items["result"] = ScanWizardResult(items["result"])
|
items["result"] = ScanWizardResult(items["result"])
|
||||||
|
|
||||||
# Process event
|
# Process event
|
||||||
if event_name == "EvtAdvertisementPacket":
|
if event_name == "EvtAdvertisementPacket":
|
||||||
scanner = self._scanners.get(items["scan_id"])
|
scanner = self._scanners.get(items["scan_id"])
|
||||||
if scanner is not None:
|
if scanner is not None:
|
||||||
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"],
|
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
|
||||||
items["is_private"], items["already_verified"])
|
|
||||||
|
|
||||||
if event_name == "EvtCreateConnectionChannelResponse":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
if items["error"] != CreateConnectionChannelError.NoError:
|
if items["error"] != CreateConnectionChannelError.NoError:
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_removed(channel, items["removed_reason"])
|
channel.on_removed(channel, items["removed_reason"])
|
||||||
|
|
||||||
if event_name == "EvtButtonUpOrDown":
|
if event_name == "EvtButtonUpOrDown":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
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"])
|
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||||
if event_name == "EvtButtonSingleOrDoubleClick":
|
if event_name == "EvtButtonSingleOrDoubleClick":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"],
|
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||||
items["time_diff"])
|
|
||||||
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"],
|
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
||||||
items["time_diff"])
|
|
||||||
|
|
||||||
if event_name == "EvtNewVerifiedButton":
|
if event_name == "EvtNewVerifiedButton":
|
||||||
self.on_new_verified_button(items["bd_addr"])
|
self.on_new_verified_button(items["bd_addr"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
self.on_get_info(items)
|
self.on_get_info(items)
|
||||||
|
|
||||||
if event_name == "EvtNoSpaceForNewConnection":
|
if event_name == "EvtNoSpaceForNewConnection":
|
||||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtGotSpaceForNewConnection":
|
if event_name == "EvtGotSpaceForNewConnection":
|
||||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
self.on_bluetooth_controller_state_change(items["state"])
|
self.on_bluetooth_controller_state_change(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
|
self.on_get_button_uuid(items["bd_addr"], items["uuid"])
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPrivateButton":
|
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_found_private_button(scan_wizard)
|
scan_wizard.on_found_private_button(scan_wizard)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPublicButton":
|
if event_name == "EvtScanWizardFoundPublicButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard._bd_addr = items["bd_addr"]
|
scan_wizard._bd_addr = items["bd_addr"]
|
||||||
scan_wizard._name = items["name"]
|
scan_wizard._name = items["name"]
|
||||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardButtonConnected":
|
if event_name == "EvtScanWizardButtonConnected":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
del 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)
|
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
def data_received(self, data):
|
|
||||||
cdata = self.buffer + data
|
def data_received(self,data):
|
||||||
self.buffer = b""
|
cdata=self.buffer+data
|
||||||
|
self.buffer=b""
|
||||||
while len(cdata):
|
while len(cdata):
|
||||||
packet_len = cdata[0] | (cdata[1] << 8)
|
packet_len = cdata[0] | (cdata[1] << 8)
|
||||||
packet_len += 2
|
packet_len += 2
|
||||||
if len(cdata) >= packet_len:
|
if len(cdata)>= packet_len:
|
||||||
self._dispatch_event(cdata[2:packet_len])
|
self._dispatch_event(cdata[2:packet_len])
|
||||||
cdata = cdata[packet_len:]
|
cdata=cdata[packet_len:]
|
||||||
else:
|
else:
|
||||||
if len(cdata):
|
if len(cdata):
|
||||||
self.buffer = cdata # unlikely to happen but.....
|
self.buffer=cdata #unlikely to happen but.....
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,12 @@ class RemovedReason(Enum):
|
||||||
RemovedByThisClient = 0
|
RemovedByThisClient = 0
|
||||||
ForceDisconnectedByThisClient = 1
|
ForceDisconnectedByThisClient = 1
|
||||||
ForceDisconnectedByOtherClient = 2
|
ForceDisconnectedByOtherClient = 2
|
||||||
|
|
||||||
ButtonIsPrivate = 3
|
ButtonIsPrivate = 3
|
||||||
VerifyTimeout = 4
|
VerifyTimeout = 4
|
||||||
InternetBackendError = 5
|
InternetBackendError = 5
|
||||||
InvalidData = 6
|
InvalidData = 6
|
||||||
|
|
||||||
CouldntLoadDevice = 7
|
CouldntLoadDevice = 7
|
||||||
|
|
||||||
class ClickType(Enum):
|
class ClickType(Enum):
|
||||||
|
@ -81,22 +81,22 @@ class ScanWizardResult(Enum):
|
||||||
|
|
||||||
class ButtonScanner:
|
class ButtonScanner:
|
||||||
"""ButtonScanner class.
|
"""ButtonScanner class.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
scanner = ButtonScanner()
|
scanner = ButtonScanner()
|
||||||
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
scanner.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: ...
|
||||||
client.add_scanner(scanner)
|
client.add_scanner(scanner)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_id = next(ButtonScanner._cnt)
|
self._scan_id = next(ButtonScanner._cnt)
|
||||||
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
|
||||||
|
|
||||||
class ScanWizard:
|
class ScanWizard:
|
||||||
"""ScanWizard class
|
"""ScanWizard class
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
wizard = ScanWizard()
|
wizard = ScanWizard()
|
||||||
wizard.on_found_private_button = lambda scan_wizard: ...
|
wizard.on_found_private_button = lambda scan_wizard: ...
|
||||||
|
@ -105,9 +105,9 @@ class ScanWizard:
|
||||||
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
wizard.on_completed = lambda scan_wizard, result, bd_addr, name: ...
|
||||||
client.add_scan_wizard(wizard)
|
client.add_scan_wizard(wizard)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_cnt = itertools.count()
|
_cnt = itertools.count()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._scan_wizard_id = next(ScanWizard._cnt)
|
self._scan_wizard_id = next(ScanWizard._cnt)
|
||||||
self._bd_addr = None
|
self._bd_addr = None
|
||||||
|
@ -119,31 +119,31 @@ class ScanWizard:
|
||||||
|
|
||||||
class ButtonConnectionChannel:
|
class ButtonConnectionChannel:
|
||||||
"""ButtonConnectionChannel class.
|
"""ButtonConnectionChannel class.
|
||||||
|
|
||||||
This class represents a connection channel to a Flic button.
|
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).
|
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.
|
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
|
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,
|
the corresponding properties to this object with a function. Each callback function has a channel parameter as the first one,
|
||||||
referencing this object.
|
referencing this object.
|
||||||
|
|
||||||
Available properties and the function parameters are:
|
Available properties and the function parameters are:
|
||||||
on_create_connection_channel_response: channel, error, connection_status
|
on_create_connection_channel_response: channel, error, connection_status
|
||||||
on_removed: channel, removed_reason
|
on_removed: channel, removed_reason
|
||||||
on_connection_status_changed: channel, connection_status, disconnect_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
|
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()
|
_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._conn_id = next(ButtonConnectionChannel._cnt)
|
||||||
self._bd_addr = bd_addr
|
self._bd_addr = bd_addr
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
self.on_create_connection_channel_response = lambda channel, error, connection_status: None
|
||||||
self.on_removed = lambda channel, removed_reason: None
|
self.on_removed = lambda channel, removed_reason: None
|
||||||
self.on_connection_status_changed = lambda channel, connection_status, disconnect_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_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 = 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
|
self.on_button_single_or_double_click_or_hold = lambda channel, click_type, was_queued, time_diff: None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bd_addr(self):
|
def bd_addr(self):
|
||||||
return self._bd_addr
|
return self._bd_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latency_mode(self):
|
def latency_mode(self):
|
||||||
return self._latency_mode
|
return self._latency_mode
|
||||||
|
|
||||||
@latency_mode.setter
|
@latency_mode.setter
|
||||||
def latency_mode(self, latency_mode):
|
def latency_mode(self, latency_mode):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._client._lock:
|
with self._client._lock:
|
||||||
self._latency_mode = latency_mode
|
self._latency_mode = latency_mode
|
||||||
if not self._client._closed:
|
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
|
@property
|
||||||
def auto_disconnect_time(self):
|
def auto_disconnect_time(self):
|
||||||
return self._auto_disconnect_time
|
return self._auto_disconnect_time
|
||||||
|
|
||||||
@auto_disconnect_time.setter
|
@auto_disconnect_time.setter
|
||||||
def auto_disconnect_time(self, auto_disconnect_time):
|
def auto_disconnect_time(self, auto_disconnect_time):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._client._lock:
|
with self._client._lock:
|
||||||
self._auto_disconnect_time = auto_disconnect_time
|
self._auto_disconnect_time = auto_disconnect_time
|
||||||
if not self._client._closed:
|
if not self._client._closed:
|
||||||
|
@ -188,26 +188,26 @@ class ButtonConnectionChannel:
|
||||||
|
|
||||||
class FlicClient:
|
class FlicClient:
|
||||||
"""FlicClient class.
|
"""FlicClient class.
|
||||||
|
|
||||||
When this class is constructed, a socket connection is established.
|
When this class is constructed, a socket connection is established.
|
||||||
You may then send commands to the server and set timers.
|
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.
|
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.
|
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 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.
|
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 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.
|
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):
|
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_new_verified_button: bd_addr
|
||||||
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
on_no_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
on_got_space_for_new_connection: max_concurrently_connected_buttons
|
||||||
on_bluetooth_controller_state_change: state
|
on_bluetooth_controller_state_change: state
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_EVENTS = [
|
_EVENTS = [
|
||||||
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
("EvtAdvertisementPacket", "<I6s17pb??", "scan_id bd_addr name rssi is_private already_verified"),
|
||||||
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
("EvtCreateConnectionChannelResponse", "<IBB", "conn_id error connection_status"),
|
||||||
|
@ -229,9 +229,9 @@ class FlicClient:
|
||||||
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
|
||||||
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
|
||||||
]
|
]
|
||||||
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _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 is None else namedtuple(x[0], x[2]), _EVENTS))
|
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
|
||||||
|
|
||||||
_COMMANDS = [
|
_COMMANDS = [
|
||||||
("CmdGetInfo", "", ""),
|
("CmdGetInfo", "", ""),
|
||||||
("CmdCreateScanner", "<I", "scan_id"),
|
("CmdCreateScanner", "<I", "scan_id"),
|
||||||
|
@ -245,19 +245,17 @@ class FlicClient:
|
||||||
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
("CmdCreateScanWizard", "<I", "scan_wizard_id"),
|
||||||
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
("CmdCancelScanWizard", "<I", "scan_wizard_id")
|
||||||
]
|
]
|
||||||
|
|
||||||
_COMMAND_STRUCTS = list(map(lambda x: struct.Struct(x[1]), _COMMANDS))
|
_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_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))
|
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
def _bdaddr_bytes_to_string(bdaddr_bytes):
|
||||||
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _bdaddr_string_to_bytes(bdaddr_string):
|
def _bdaddr_string_to_bytes(bdaddr_string):
|
||||||
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
|
||||||
|
|
||||||
def __init__(self, host, port = 5551):
|
def __init__(self, host, port = 5551):
|
||||||
self._sock = socket.create_connection((host, port), None)
|
self._sock = socket.create_connection((host, port), None)
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
@ -269,113 +267,113 @@ class FlicClient:
|
||||||
self._timers = queue.PriorityQueue()
|
self._timers = queue.PriorityQueue()
|
||||||
self._handle_event_thread_ident = None
|
self._handle_event_thread_ident = None
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
self.on_new_verified_button = lambda bd_addr: None
|
self.on_new_verified_button = lambda bd_addr: None
|
||||||
self.on_no_space_for_new_connection = lambda max_concurrently_connected_buttons: 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_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
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closes the client. The handle_events() method will return."""
|
"""Closes the client. The handle_events() method will return."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
if threading.get_ident() != self._handle_event_thread_ident:
|
if threading.get_ident() != self._handle_event_thread_ident:
|
||||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||||
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def add_scanner(self, scanner):
|
def add_scanner(self, scanner):
|
||||||
"""Add a ButtonScanner object.
|
"""Add a ButtonScanner object.
|
||||||
|
|
||||||
The scan will start directly once the scanner is added.
|
The scan will start directly once the scanner is added.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scanner._scan_id in self._scanners:
|
if scanner._scan_id in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scanners[scanner._scan_id] = scanner
|
self._scanners[scanner._scan_id] = scanner
|
||||||
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdCreateScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def remove_scanner(self, scanner):
|
def remove_scanner(self, scanner):
|
||||||
"""Remove a ButtonScanner object.
|
"""Remove a ButtonScanner object.
|
||||||
|
|
||||||
You will no longer receive advertisement packets.
|
You will no longer receive advertisement packets.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scanner._scan_id not in self._scanners:
|
if scanner._scan_id not in self._scanners:
|
||||||
return
|
return
|
||||||
|
|
||||||
del self._scanners[scanner._scan_id]
|
del self._scanners[scanner._scan_id]
|
||||||
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
self._send_command("CmdRemoveScanner", {"scan_id": scanner._scan_id})
|
||||||
|
|
||||||
def add_scan_wizard(self, scan_wizard):
|
def add_scan_wizard(self, scan_wizard):
|
||||||
"""Add a ScanWizard object.
|
"""Add a ScanWizard object.
|
||||||
|
|
||||||
The scan wizard will start directly once the scan wizard is added.
|
The scan wizard will start directly once the scan wizard is added.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
if scan_wizard._scan_wizard_id in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
self._scan_wizards[scan_wizard._scan_wizard_id] = scan_wizard
|
||||||
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCreateScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def cancel_scan_wizard(self, scan_wizard):
|
def cancel_scan_wizard(self, scan_wizard):
|
||||||
"""Cancel a ScanWizard.
|
"""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.
|
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 cancelled due to this command, "result" in the on_completed event will be "WizardCancelledByUser".
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
if scan_wizard._scan_wizard_id not in self._scan_wizards:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
self._send_command("CmdCancelScanWizard", {"scan_wizard_id": scan_wizard._scan_wizard_id})
|
||||||
|
|
||||||
def add_connection_channel(self, channel):
|
def add_connection_channel(self, channel):
|
||||||
"""Adds a connection channel to a specific Flic button.
|
"""Adds a connection channel to a specific Flic button.
|
||||||
|
|
||||||
This will start listening for a specific Flic button's connection and button events.
|
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.
|
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
|
The on_create_connection_channel_response callback property will be called on the
|
||||||
connection channel after this command has been received by the server.
|
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.
|
You may have as many connection channels as you wish for a specific Flic Button.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if channel._conn_id in self._connection_channels:
|
if channel._conn_id in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
channel._client = self
|
channel._client = self
|
||||||
|
|
||||||
self._connection_channels[channel._conn_id] = channel
|
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):
|
def remove_connection_channel(self, channel):
|
||||||
"""Remove a connection channel.
|
"""Remove a connection channel.
|
||||||
|
|
||||||
This will stop listening for new events for a specific connection channel that has previously been added.
|
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.
|
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:
|
with self._lock:
|
||||||
if channel._conn_id not in self._connection_channels:
|
if channel._conn_id not in self._connection_channels:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
self._send_command("CmdRemoveConnectionChannel", {"conn_id": channel._conn_id})
|
||||||
|
|
||||||
def force_disconnect(self, bd_addr):
|
def force_disconnect(self, bd_addr):
|
||||||
"""Force disconnection or cancel pending connection of a specific Flic button.
|
"""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.
|
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})
|
self._send_command("CmdForceDisconnect", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def get_info(self, callback):
|
def get_info(self, callback):
|
||||||
"""Get info about the current state of the server.
|
"""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 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:
|
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,
|
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._get_info_response_queue.put(callback)
|
||||||
self._send_command("CmdGetInfo", {})
|
self._send_command("CmdGetInfo", {})
|
||||||
|
|
||||||
def get_button_uuid(self, bd_addr, callback):
|
def get_button_uuid(self, bd_addr, callback):
|
||||||
"""Get button uuid for a verified button.
|
"""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.
|
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.
|
Responses will arrive in the same order as requested.
|
||||||
|
|
||||||
The callback takes two parameters: bd_addr, uuid (hex string of 32 characters).
|
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.
|
Note: if the button isn't verified, the uuid sent to the callback will rather be None.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._get_button_uuid_queue.put(callback)
|
self._get_button_uuid_queue.put(callback)
|
||||||
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
|
||||||
|
|
||||||
def set_timer(self, timeout_millis, callback):
|
def set_timer(self, timeout_millis, callback):
|
||||||
"""Set a timer
|
"""Set a timer
|
||||||
|
|
||||||
This timer callback will run after the specified timeout_millis on the thread that handles the events.
|
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
|
point_in_time = time.monotonic() + timeout_millis / 1000.0
|
||||||
self._timers.put((point_in_time, callback))
|
self._timers.put((point_in_time, callback))
|
||||||
|
|
||||||
if threading.get_ident() != self._handle_event_thread_ident:
|
if threading.get_ident() != self._handle_event_thread_ident:
|
||||||
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
self._send_command("CmdPing", {"ping_id": 0}) # To unblock socket select
|
||||||
|
|
||||||
def run_on_handle_events_thread(self, callback):
|
def run_on_handle_events_thread(self, callback):
|
||||||
"""Run a function on the thread that handles the events."""
|
"""Run a function on the thread that handles the events."""
|
||||||
if threading.get_ident() == self._handle_event_thread_ident:
|
if threading.get_ident() == self._handle_event_thread_ident:
|
||||||
callback()
|
callback()
|
||||||
else:
|
else:
|
||||||
self.set_timer(0, callback)
|
self.set_timer(0, callback)
|
||||||
|
|
||||||
def _send_command(self, name, items):
|
def _send_command(self, name, items):
|
||||||
for key, value in items.items():
|
for key, value in items.items():
|
||||||
if isinstance(value, Enum):
|
if isinstance(value, Enum):
|
||||||
items[key] = value.value
|
items[key] = value.value
|
||||||
|
|
||||||
if "bd_addr" in items:
|
if "bd_addr" in items:
|
||||||
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
|
||||||
|
|
||||||
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
|
||||||
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
|
||||||
bytes = bytearray(3)
|
bytes = bytearray(3)
|
||||||
|
@ -434,83 +432,83 @@ class FlicClient:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if not self._closed:
|
if not self._closed:
|
||||||
self._sock.sendall(bytes)
|
self._sock.sendall(bytes)
|
||||||
|
|
||||||
def _dispatch_event(self, data):
|
def _dispatch_event(self, data):
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
opcode = data[0]
|
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
|
return
|
||||||
|
|
||||||
event_name = FlicClient._EVENTS[opcode][0]
|
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()
|
items = FlicClient._EVENT_NAMED_TUPLES[opcode]._make(data_tuple)._asdict()
|
||||||
|
|
||||||
# Process some kind of items whose data type is not supported by struct
|
# Process some kind of items whose data type is not supported by struct
|
||||||
if "bd_addr" in items:
|
if "bd_addr" in items:
|
||||||
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
|
||||||
|
|
||||||
if "name" in items:
|
if "name" in items:
|
||||||
items["name"] = items["name"].decode("utf-8")
|
items["name"] = items["name"].decode("utf-8")
|
||||||
|
|
||||||
if event_name == "EvtCreateConnectionChannelResponse":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
items["error"] = CreateConnectionChannelError(items["error"])
|
items["error"] = CreateConnectionChannelError(items["error"])
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
items["connection_status"] = ConnectionStatus(items["connection_status"])
|
||||||
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
items["disconnect_reason"] = DisconnectReason(items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
items["removed_reason"] = RemovedReason(items["removed_reason"])
|
||||||
|
|
||||||
if event_name.startswith("EvtButton"):
|
if event_name.startswith("EvtButton"):
|
||||||
items["click_type"] = ClickType(items["click_type"])
|
items["click_type"] = ClickType(items["click_type"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
|
||||||
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
|
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
|
||||||
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
|
||||||
items["bd_addr_of_verified_buttons"] = []
|
items["bd_addr_of_verified_buttons"] = []
|
||||||
|
|
||||||
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
pos = FlicClient._EVENT_STRUCTS[opcode].size
|
||||||
for i in range(items["nb_verified_buttons"]):
|
for i in range(items["nb_verified_buttons"]):
|
||||||
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
|
||||||
pos += 6
|
pos += 6
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
items["state"] = BluetoothControllerState(items["state"])
|
items["state"] = BluetoothControllerState(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
items["uuid"] = "".join(map(lambda x: "%02x" % x, items["uuid"]))
|
||||||
if items["uuid"] == "00000000000000000000000000000000":
|
if items["uuid"] == "00000000000000000000000000000000":
|
||||||
items["uuid"] = None
|
items["uuid"] = None
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
items["result"] = ScanWizardResult(items["result"])
|
items["result"] = ScanWizardResult(items["result"])
|
||||||
|
|
||||||
# Process event
|
# Process event
|
||||||
if event_name == "EvtAdvertisementPacket":
|
if event_name == "EvtAdvertisementPacket":
|
||||||
scanner = self._scanners.get(items["scan_id"])
|
scanner = self._scanners.get(items["scan_id"])
|
||||||
if scanner is not None:
|
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":
|
if event_name == "EvtCreateConnectionChannelResponse":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
if items["error"] != CreateConnectionChannelError.NoError:
|
if items["error"] != CreateConnectionChannelError.NoError:
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
channel.on_create_connection_channel_response(channel, items["error"], items["connection_status"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionStatusChanged":
|
if event_name == "EvtConnectionStatusChanged":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
channel.on_connection_status_changed(channel, items["connection_status"], items["disconnect_reason"])
|
||||||
|
|
||||||
if event_name == "EvtConnectionChannelRemoved":
|
if event_name == "EvtConnectionChannelRemoved":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
del self._connection_channels[items["conn_id"]]
|
del self._connection_channels[items["conn_id"]]
|
||||||
channel.on_removed(channel, items["removed_reason"])
|
channel.on_removed(channel, items["removed_reason"])
|
||||||
|
|
||||||
if event_name == "EvtButtonUpOrDown":
|
if event_name == "EvtButtonUpOrDown":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
channel = self._connection_channels[items["conn_id"]]
|
||||||
channel.on_button_up_or_down(channel, items["click_type"], items["was_queued"], items["time_diff"])
|
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":
|
if event_name == "EvtButtonSingleOrDoubleClickOrHold":
|
||||||
channel = self._connection_channels[items["conn_id"]]
|
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":
|
if event_name == "EvtNewVerifiedButton":
|
||||||
self.on_new_verified_button(items["bd_addr"])
|
self.on_new_verified_button(items["bd_addr"])
|
||||||
|
|
||||||
if event_name == "EvtGetInfoResponse":
|
if event_name == "EvtGetInfoResponse":
|
||||||
self._get_info_response_queue.get()(items)
|
self._get_info_response_queue.get()(items)
|
||||||
|
|
||||||
if event_name == "EvtNoSpaceForNewConnection":
|
if event_name == "EvtNoSpaceForNewConnection":
|
||||||
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_no_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtGotSpaceForNewConnection":
|
if event_name == "EvtGotSpaceForNewConnection":
|
||||||
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
self.on_got_space_for_new_connection(items["max_concurrently_connected_buttons"])
|
||||||
|
|
||||||
if event_name == "EvtBluetoothControllerStateChange":
|
if event_name == "EvtBluetoothControllerStateChange":
|
||||||
self.on_bluetooth_controller_state_change(items["state"])
|
self.on_bluetooth_controller_state_change(items["state"])
|
||||||
|
|
||||||
if event_name == "EvtGetButtonUUIDResponse":
|
if event_name == "EvtGetButtonUUIDResponse":
|
||||||
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
|
self._get_button_uuid_queue.get()(items["bd_addr"], items["uuid"])
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPrivateButton":
|
if event_name == "EvtScanWizardFoundPrivateButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_found_private_button(scan_wizard)
|
scan_wizard.on_found_private_button(scan_wizard)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardFoundPublicButton":
|
if event_name == "EvtScanWizardFoundPublicButton":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard._bd_addr = items["bd_addr"]
|
scan_wizard._bd_addr = items["bd_addr"]
|
||||||
scan_wizard._name = items["name"]
|
scan_wizard._name = items["name"]
|
||||||
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_found_public_button(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardButtonConnected":
|
if event_name == "EvtScanWizardButtonConnected":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
scan_wizard.on_button_connected(scan_wizard, scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
if event_name == "EvtScanWizardCompleted":
|
if event_name == "EvtScanWizardCompleted":
|
||||||
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
scan_wizard = self._scan_wizards[items["scan_wizard_id"]]
|
||||||
del 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)
|
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
|
||||||
|
|
||||||
def _handle_one_event(self):
|
def _handle_one_event(self):
|
||||||
if len(self._timers.queue) > 0:
|
if len(self._timers.queue) > 0:
|
||||||
current_timer = self._timers.queue[0]
|
current_timer = self._timers.queue[0]
|
||||||
|
@ -570,10 +568,10 @@ class FlicClient:
|
||||||
return True
|
return True
|
||||||
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
|
if len(select.select([self._sock], [], [], timeout)[0]) == 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
len_arr = bytearray(2)
|
len_arr = bytearray(2)
|
||||||
view = memoryview(len_arr)
|
view = memoryview(len_arr)
|
||||||
|
|
||||||
toread = 2
|
toread = 2
|
||||||
while toread > 0:
|
while toread > 0:
|
||||||
nbytes = self._sock.recv_into(view, toread)
|
nbytes = self._sock.recv_into(view, toread)
|
||||||
|
@ -581,7 +579,7 @@ class FlicClient:
|
||||||
return False
|
return False
|
||||||
view = view[nbytes:]
|
view = view[nbytes:]
|
||||||
toread -= nbytes
|
toread -= nbytes
|
||||||
|
|
||||||
packet_len = len_arr[0] | (len_arr[1] << 8)
|
packet_len = len_arr[0] | (len_arr[1] << 8)
|
||||||
data = bytearray(packet_len)
|
data = bytearray(packet_len)
|
||||||
view = memoryview(data)
|
view = memoryview(data)
|
||||||
|
@ -592,13 +590,13 @@ class FlicClient:
|
||||||
return False
|
return False
|
||||||
view = view[nbytes:]
|
view = view[nbytes:]
|
||||||
toread -= nbytes
|
toread -= nbytes
|
||||||
|
|
||||||
self._dispatch_event(data)
|
self._dispatch_event(data)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def handle_events(self):
|
def handle_events(self):
|
||||||
"""Start the main loop for this client.
|
"""Start the main loop for this client.
|
||||||
|
|
||||||
This method will not return until the socket has been closed.
|
This method will not return until the socket has been closed.
|
||||||
Once it has returned, any use of this FlicClient is illegal.
|
Once it has returned, any use of this FlicClient is illegal.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -19,10 +19,6 @@ class CameraPiBackend(Backend):
|
||||||
|
|
||||||
* **picamera** (``pip install picamera``)
|
* **picamera** (``pip install picamera``)
|
||||||
* **redis** (``pip install redis``) for inter-process communication with the camera process
|
* **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):
|
class CameraAction(Enum):
|
||||||
|
@ -34,7 +30,7 @@ class CameraPiBackend(Backend):
|
||||||
return self.value == other
|
return self.value == other
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
# 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',
|
redis_queue='platypush/camera/pi',
|
||||||
start_recording_on_startup=True,
|
start_recording_on_startup=True,
|
||||||
framerate=24, hflip=False, vflip=False,
|
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
|
:param listen_port: Port where the camera process will provide the video output while recording
|
||||||
:type listen_port: int
|
:type listen_port: int
|
||||||
|
|
||||||
:param bind_address: Bind address (default: 0.0.0.0).
|
|
||||||
:type bind_address: str
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.bind_address = bind_address
|
|
||||||
self.listen_port = listen_port
|
self.listen_port = listen_port
|
||||||
self.server_socket = socket.socket()
|
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)
|
self.server_socket.listen(0)
|
||||||
|
|
||||||
import picamera
|
import picamera
|
||||||
|
@ -126,7 +118,7 @@ class CameraPiBackend(Backend):
|
||||||
while True:
|
while True:
|
||||||
self.camera.wait_recording(2)
|
self.camera.wait_recording(2)
|
||||||
else:
|
else:
|
||||||
while not self.should_stop():
|
while True:
|
||||||
connection = self.server_socket.accept()[0].makefile('wb')
|
connection = self.server_socket.accept()[0].makefile('wb')
|
||||||
self.logger.info('Accepted client connection on port {}'.format(self.listen_port))
|
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')
|
self.logger.info('Client closed connection')
|
||||||
try:
|
try:
|
||||||
self.stop_recording()
|
self.stop_recording()
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.warning('Could not stop recording: {}'.format(str(e)))
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connection.close()
|
connection.close()
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.warning('Could not close connection: {}'.format(str(e)))
|
pass
|
||||||
|
|
||||||
self.send_camera_action(self.CameraAction.START_RECORDING)
|
self.send_camera_action(self.CameraAction.START_RECORDING)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ class ClipboardBackend(Backend):
|
||||||
self._last_text: Optional[str] = None
|
self._last_text: Optional[str] = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.logger.info('Started clipboard monitor backend')
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
text = pyperclip.paste()
|
text = pyperclip.paste()
|
||||||
if text and text != self._last_text:
|
if text and text != self._last_text:
|
||||||
|
@ -35,6 +34,5 @@ class ClipboardBackend(Backend):
|
||||||
self._last_text = text
|
self._last_text = text
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
self.logger.info('Stopped clipboard monitor backend')
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
import dbus
|
|
||||||
import dbus.service
|
|
||||||
import dbus.mainloop.glib
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.context import get_bus
|
|
||||||
from platypush.message import Message
|
|
||||||
from platypush.message.event import Event
|
|
||||||
from platypush.message.request import Request
|
|
||||||
from platypush.utils import run
|
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyPep8Naming
|
|
||||||
class DBusService(dbus.service.Object):
|
|
||||||
@classmethod
|
|
||||||
def _parse_msg(cls, msg: Union[dict, list]):
|
|
||||||
import json
|
|
||||||
return Message.build(json.loads(json.dumps(msg)))
|
|
||||||
|
|
||||||
@dbus.service.method('org.platypush.MessageBusInterface', in_signature='a{sv}', out_signature='v')
|
|
||||||
def Post(self, msg: dict):
|
|
||||||
"""
|
|
||||||
This method accepts a message as a dictionary (either representing a valid request or an event) and either
|
|
||||||
executes it (request) or forwards it to the application bus (event).
|
|
||||||
|
|
||||||
:param msg: Request or event, as a dictionary.
|
|
||||||
:return: The return value of the request, or 0 if the message is an event.
|
|
||||||
"""
|
|
||||||
msg = self._parse_msg(msg)
|
|
||||||
if isinstance(msg, Request):
|
|
||||||
ret = run(msg.action, **msg.args)
|
|
||||||
if ret is None:
|
|
||||||
ret = '' # DBus doesn't like None return types
|
|
||||||
|
|
||||||
return ret
|
|
||||||
elif isinstance(msg, Event):
|
|
||||||
get_bus().post(msg)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class DbusBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend acts as a proxy that receives messages (requests or events) on the DBus and forwards them to the
|
|
||||||
application bus.
|
|
||||||
|
|
||||||
The name of the messaging interface exposed by Platypush is ``org.platypush.MessageBusInterface`` and it exposes
|
|
||||||
``Post`` method, which accepts a dictionary representing a valid Platypush message (either a request or an event)
|
|
||||||
and either executes it or forwards it to the application bus.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* **dbus-python** (``pip install dbus-python``)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bus_name='org.platypush.Bus', service_path='/MessageService', *args, **kwargs):
|
|
||||||
"""
|
|
||||||
:param bus_name: Name of the bus where the application will listen for incoming messages (default:
|
|
||||||
``org.platypush.Bus``).
|
|
||||||
:param service_path: Path to the service exposed by the app (default: ``/MessageService``).
|
|
||||||
"""
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.bus_name = bus_name
|
|
||||||
self.service_path = service_path
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
|
||||||
bus = dbus.SessionBus()
|
|
||||||
name = dbus.service.BusName(self.bus_name, bus)
|
|
||||||
srv = DBusService(bus, self.service_path)
|
|
||||||
|
|
||||||
loop = GLib.MainLoop()
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
self.logger.info('Starting DBus main loop - bus name: {}, service: {}'.format(name._name, srv._object_path))
|
|
||||||
loop.run()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,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')
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue