Adding backends documentation

This commit is contained in:
Fabio Manganiello 2018-06-26 00:16:39 +02:00
parent 85c7faf21b
commit 28862d743d
36 changed files with 592 additions and 167 deletions

25
docs/source/backends.rst Normal file
View file

@ -0,0 +1,25 @@
Backends
========
.. toctree::
:maxdepth: 2
:caption: Backends:
platypush/backend.rst
platypush/backend/assistant.google.rst
platypush/backend/assistant.google.pushtotalk.rst
platypush/backend/assistant.snowboy.rst
platypush/backend/button.flic.rst
platypush/backend/camera.pi.rst
platypush/backend/http.rst
platypush/backend/http.poll.rst
platypush/backend/inotify.rst
platypush/backend/kafka.rst
platypush/backend/midi.rst
platypush/backend/mqtt.rst
platypush/backend/music.mpd.rst
platypush/backend/pushbullet.rst
platypush/backend/redis.rst
platypush/backend/scard.rst
platypush/backend/sensor.rst

View file

@ -5,6 +5,7 @@ Platypush
:maxdepth: 3
:caption: Contents:
backends
plugins
Indices and tables

View file

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

View file

@ -0,0 +1,6 @@
``platypush.backend.assistant.google.pushtotalk``
=================================================
.. automodule:: platypush.backend.assistant.google.pushtotalk
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.assistant.google``
======================================
.. automodule:: platypush.backend.assistant.google
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.assistant.snowboy``
=======================================
.. automodule:: platypush.backend.assistant.snowboy
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.button.flic``
=================================
.. automodule:: platypush.backend.button.flic
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.camera.pi``
===============================
.. automodule:: platypush.backend.camera.pi
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.http.poll``
===============================
.. automodule:: platypush.backend.http.poll
:members:

View file

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

View file

@ -0,0 +1,6 @@
``platypush.backend.inotify``
=============================
.. automodule:: platypush.backend.inotify
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.kafka``
===========================
.. automodule:: platypush.backend.kafka
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.midi``
==========================
.. automodule:: platypush.backend.midi
:members:

View file

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

View file

@ -0,0 +1,6 @@
``platypush.backend.music.mpd``
===============================
.. automodule:: platypush.backend.music.mpd
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.pushbullet``
================================
.. automodule:: platypush.backend.pushbullet
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.redis``
===========================
.. automodule:: platypush.backend.redis
:members:

View file

@ -0,0 +1,6 @@
``platypush.backend.scard``
===========================
.. automodule:: platypush.backend.scard
:members:

View file

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

View file

@ -16,16 +16,26 @@ from platypush.message.response import Response
class Backend(Thread):
""" Parent class for backends """
"""
Parent class for backends.
A backend is basically a thread that checks for new events on some channel
(e.g. a network socket, a queue, some new entries on an API endpoint or an
RSS feed, a voice command through an assistant, a new measure from a sensor
etc.) and propagates event messages to the main application bus whenever a
new event happens. You can then build whichever type of custom logic you
want on such events.
"""
_default_response_timeout = 5
def __init__(self, bus=None, **kwargs):
"""
Params:
bus -- Reference to the Platypush bus where the requests and the
responses will be posted [Bus]
kwargs -- key-value configuration for this backend [Dict]
:param bus: Reference to the bus object to be used in the backend
:type bus: platypush.bus.Bus
:param kwargs: Key-value configuration for the backend
:type kwargs: dict
"""
# If no bus is specified, create an internal queue where
@ -55,11 +65,9 @@ class Backend(Thread):
It should be called by the derived classes whenever
a new message should be processed.
Params:
msg -- The message. It can be either a key-value
dictionary, a platypush.message.Message
object, or a string/byte UTF-8 encoded string
:param msg: Received message. It can be either a key-value dictionary, a platypush.message.Message object, or a string/byte UTF-8 encoded string
"""
msg = Message.build(msg)
if not getattr(msg, 'target') or msg.target != self.device_id:
@ -120,10 +128,9 @@ class Backend(Thread):
def send_event(self, event, **kwargs):
"""
Send an event message on the backend
Params:
event -- The request, either a dict, a string/bytes UTF-8 JSON,
or a platypush.message.event.Event object.
Send an event message on the backend.
:param event: Event to send. It can be a dict, a string/bytes UTF-8 JSON, or a platypush.message.event.Event object.
"""
event = Event.build(event)
@ -139,17 +146,15 @@ class Backend(Thread):
def send_request(self, request, on_response=None,
response_timeout=_default_response_timeout, **kwargs):
"""
Send a request message on the backend
Params:
request -- The request, either a dict, a string/bytes UTF-8 JSON,
or a platypush.message.request.Request object.
Send a request message on the backend.
on_response -- Response handler, takes a platypush.message.response.Response
as argument. If set, the method will wait for a
response before exiting (default: None)
response_timeout -- If on_response is set, the backend will raise
an exception if the response isn't received
within this number of seconds (default: 5)
:param request: The request, either a dict, a string/bytes UTF-8 JSON, or a platypush.message.request.Request object.
:param on_response: Optional callback that will be called when a response is received. If set, this method will synchronously wait for a response before exiting.
:type on_response: function
:param response_timeout: If on_response is set, the backend will raise an exception if the response isn't received within this number of seconds (default: None)
:type response_timeout: float
"""
request = Request.build(request)
@ -165,12 +170,10 @@ class Backend(Thread):
def send_response(self, response, request, **kwargs):
"""
Send a response message on the backend
Params:
response -- The response, either a dict, a string/bytes UTF-8 JSON,
or a platypush.message.response.Response object
request -- Associated request, used to set the response parameters
that will link them
Send a response message on the backend.
:param response: The response, either a dict, a string/bytes UTF-8 JSON, or a platypush.message.response.Response object
:param request: Associated request, used to set the response parameters that will link them
"""
response = Response.build(response)
@ -191,8 +194,7 @@ class Backend(Thread):
backend is configured then it will try to deliver the message to
other consumers through the configured Redis main queue.
Params:
msg -- The message
:param msg: The message to send
"""
try:
redis = get_backend('redis')

View file

@ -16,18 +16,35 @@ from platypush.message.event.assistant import \
class AssistantGoogleBackend(Backend):
""" Class for the Google Assistant backend. It creates and event source
that posts recognized phrases on the main bus """
"""
Google Assistant backend.
It listens for voice commands and post conversation events on the bus.
Triggers:
* :class:`platypush.message.event.assistant.ConversationStartEvent` when a new conversation starts
* :class:`platypush.message.event.assistant.SpeechRecognizedEvent` when a new voice command is recognized
* :class:`platypush.message.event.assistant.NoResponse` when a conversation returned no response
* :class:`platypush.message.event.assistant.ResponseEvent` when the assistant is speaking a response
* :class:`platypush.message.event.assistant.ConversationTimeoutEvent` when a conversation times out
* :class:`platypush.message.event.assistant.ConversationEndEvent` when a new conversation ends
Requires:
* **google-assistant-sdk** (``pip install google-assistant-sdk``)
"""
def __init__(self, credentials_file=os.path.join(
os.path.expanduser('~/.config'),
'google-oauthlib-tool', 'credentials.json'),
device_model_id='Platypush', **kwargs):
""" Params:
credentials_file -- Path to the Google OAuth credentials file
(default: ~/.config/google-oauthlib-tool/credentials.json)
device_model_id -- Device model ID to use for the assistant
(default: Platypush)
"""
:param credentials_file: Path to the Google OAuth credentials file (default: ~/.config/google-oauthlib-tool/credentials.json). See https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials for how to get your own credentials file.
:type credentials_file: str
:param device_model_id: Device model ID to use for the assistant (default: Platypush)
:type device_model_id: str
"""
super().__init__(**kwargs)
@ -61,20 +78,15 @@ class AssistantGoogleBackend(Backend):
def start_conversation(self):
""" Starts an assistant conversation """
if self.assistant: self.assistant.start_conversation()
def stop_conversation(self):
""" Stops an assistant conversation """
if self.assistant: self.assistant.stop_conversation()
def send_message(self, msg):
# Can't send a message on an event source, ignoring
# TODO Make a class for event sources like these. Event sources
# would be a subset of the backends which can fire events on the bus
# but not receive requests or process responses.
pass
def run(self):
super().run()

View file

@ -25,12 +25,25 @@ from platypush.message.event.assistant import \
class AssistantGooglePushtotalkBackend(Backend):
""" Google Assistant pushtotalk backend. Instead of listening for
the "OK Google" hotword like the assistant.google backend,
this implementation programmatically starts a conversation
upon start_conversation() method call. Use this backend on
devices that don't have an Assistant SDK package (e.g. arm6 devices
like the RaspberryPi Zero or the RaspberryPi 1) """
"""
Google Assistant pushtotalk backend. Instead of listening for the "OK
Google" hotword like the assistant.google backend, this implementation
programmatically starts a conversation upon start_conversation() method
call. Use this backend on devices that don't have an Assistant SDK package
(e.g. arm6 devices like the RaspberryPi Zero or the RaspberryPi 1).
Triggers:
* :class:`platypush.message.event.assistant.ConversationStartEvent` when a new conversation starts
* :class:`platypush.message.event.assistant.SpeechRecognizedEvent` when a new voice command is recognized
* :class:`platypush.message.event.assistant.ConversationEndEvent` when a new conversation ends
Requires:
* **tenacity** (``pip install tenacity``)
* **grpc** (``pip install grpc``)
* **google-assistant-grpc** (``pip install google-assistant-grpc``)
"""
api_endpoint = 'embeddedassistant.googleapis.com'
audio_sample_rate = audio_helpers.DEFAULT_AUDIO_SAMPLE_RATE
@ -49,13 +62,15 @@ class AssistantGooglePushtotalkBackend(Backend):
lang='en-US',
conversation_start_fifo = os.path.join(os.path.sep, 'tmp', 'pushtotalk.fifo'),
*args, **kwargs):
""" Params:
credentials_file -- Path to the Google OAuth credentials file
(default: ~/.config/google-oauthlib-tool/credentials.json)
device_config -- Path to device_config.json. Register your
device and create a project, then run the pushtotalk.py
script from googlesamples to create your device_config.json
lang -- Assistant language (default: en-US)
"""
:param credentials_file: Path to the Google OAuth credentials file (default: ~/.config/google-oauthlib-tool/credentials.json). See https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials for how to get your own credentials file.
:type credentials_file: str
:param device_config: Path to device_config.json. Register your device (see https://developers.google.com/assistant/sdk/guides/library/python/embed/register-device) and create a project, then run the pushtotalk.py script from googlesamples to create your device_config.json
:type device_config: str
:param lang: Assistant language (default: en-US)
:type lang: str
"""
super().__init__(*args, **kwargs)
@ -125,11 +140,13 @@ class AssistantGooglePushtotalkBackend(Backend):
self.device_handler = device_helpers.DeviceRequestHandler(self.device_id)
def start_conversation(self):
""" Start a conversation """
if self.assistant:
with open(self.conversation_start_fifo, 'w') as f:
f.write('1')
def stop_conversation(self):
""" Stop a conversation """
if self.assistant:
self.conversation_stream.stop_playback()
self.bus.post(ConversationEndEvent())
@ -345,5 +362,6 @@ class SampleAssistant(object):
# Subsequent requests need audio data, but not config.
yield embedded_assistant_pb2.AssistRequest(audio_in=data)
# vim:sw=4:ts=4:et:

View file

@ -3,28 +3,46 @@ import os
import subprocess
import time
from snowboy import snowboydecoder
from platypush.backend import Backend
from platypush.message.event.assistant import \
ConversationStartEvent, ConversationEndEvent, \
SpeechRecognizedEvent, HotwordDetectedEvent
class AssistantSnowboyBackend(Backend):
""" Backend for detecting custom voice hotwords through Snowboy.
The purpose of this component is only to detect the hotword
specified in your Snowboy voice model. If you want to trigger
proper assistant conversations or custom speech recognition,
you should create a hook in your configuration on HotwordDetectedEvent
to trigger the conversation on whichever assistant plugin you're using
(Google, Alexa...) """
"""
Backend for detecting custom voice hotwords through Snowboy. The purpose of
this component is only to detect the hotword specified in your Snowboy voice
model. If you want to trigger proper assistant conversations or custom
speech recognition, you should create a hook in your configuration on
HotwordDetectedEvent to trigger the conversation on whichever assistant
plugin you're using (Google, Alexa...)
Triggers:
* :class:`platypush.message.event.assistant.HotwordDetectedEvent` whenever the hotword has been detected
Requires:
* **snowboy** (``pip install snowboy``)
"""
def __init__(self, voice_model_file, hotword=None, sensitivity=0.5,
audio_gain=1.0, **kwargs):
""" Params:
voice_model_file -- Snowboy voice model file
hotword -- Name of the hotword
"""
:param voice_model_file: Snowboy voice model file - see https://snowboy.kitt.ai/
:type voice_model_file: str
:param hotword: Name of the hotword
:type hotword: str
:param sensitivity: Hotword recognition sensitivity, between 0 and 1
:type sensitivity: float
:param audio_gain: Audio gain, between 0 and 1
:type audio_gain: float
"""
from snowboy import snowboydecoder
super().__init__(**kwargs)
self.voice_model_file = voice_model_file
@ -38,9 +56,6 @@ class AssistantSnowboyBackend(Backend):
self.logger.info('Initialized Snowboy hotword detection')
def send_message(self, msg):
pass
def hotword_detected(self):
def callback():
self.bus.post(HotwordDetectedEvent(hotword=self.hotword))

View file

@ -11,13 +11,37 @@ from .fliclib.fliclib import FlicClient, ButtonConnectionChannel, ClickType
class ButtonFlicBackend(Backend):
"""
Backend that listen for events from the Flic (https://flic.io/) bluetooth
smart buttons.
Triggers:
* :class:`platypush.message.event.button.flic.FlicButtonEvent` when a button is pressed. The event will also contain the press sequence (e.g. ``["ShortPressEvent", "LongPressEvent", "ShortPressEvent"]``)
Requires:
* **fliclib** (https://github.com/50ButtonsEach/fliclib-linux-hci). For the backend to work properly you need to have the ``flicd`` daemon from the fliclib running, and you have to first pair the buttons with your device using any of the scanners provided by the library.
"""
_long_press_timeout = 0.3
_btn_timeout = 0.5
ShortPressEvent = "ShortPressEvent"
LongPressEvent = "LongPressEvent"
def __init__(self, server, long_press_timeout=_long_press_timeout,
def __init__(self, server='localhost', long_press_timeout=_long_press_timeout,
btn_timeout=_btn_timeout, **kwargs):
"""
:param server: flicd server host (default: localhost)
:type server: str
:param long_press_timeout: How long you should press a button for a press action to be considered "long press" (default: 0.3 secohds)
:type long_press_timeout: float
:param btn_timeout: How long since the last button release before considering the user interaction completed (default: 0.5 seconds)
:type btn_timeout: float
"""
super().__init__(**kwargs)
self.server = server
@ -88,9 +112,6 @@ class ButtonFlicBackend(Backend):
return _f
def send_message(self, msg):
pass
def run(self):
super().run()

View file

@ -1,7 +1,6 @@
import json
import socket
import time
import picamera
from enum import Enum
from redis import Redis
@ -10,6 +9,17 @@ from threading import Thread
from platypush.backend import Backend
class CameraPiBackend(Backend):
"""
Backend to interact with a Raspberry Pi camera. It can start and stop
recordings and take pictures. It can be programmatically controlled through
the :class:`platypush.plugins.camera.pi` plugin.
Requires:
* **picamera** (``pip install picamera``)
* **redis** (``pip install redis``) for inter-process communication with the camera process
"""
class CameraAction(Enum):
START_RECORDING = 'START_RECORDING'
STOP_RECORDING = 'STOP_RECORDING'
@ -27,9 +37,15 @@ class CameraPiBackend(Backend):
exposure_mode='auto', meter_mode='average', awb_mode='auto',
image_effect='none', color_effects=None, rotation=0,
crop=(0.0, 0.0, 1.0, 1.0), **kwargs):
""" See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
for a detailed reference about the Pi camera options """
"""
See https://www.raspberrypi.org/documentation/usage/camera/python/README.md
for a detailed reference about the Pi camera options.
:param listen_port: Port where the camera process will provide the video output while recording
:type listen_port: int
"""
import picamera
super().__init__(**kwargs)
self.listen_port = listen_port
@ -74,14 +90,30 @@ class CameraPiBackend(Backend):
self.redis.rpush(self.redis_queue, json.dumps(action))
def take_picture(self, image_file):
"""
Take a picture.
:param image_file: Output image file
:type image_file: str
"""
self.logger.info('Capturing camera snapshot to {}'.format(image_file))
self.camera.capture(image_file)
self.logger.info('Captured camera snapshot to {}'.format(image_file))
def start_recording(self, video_file=None, format='h264'):
"""
Start a recording.
:param video_file: Output video file. If specified, the video will be recorded to file, otherwise it will be served via TCP/IP on the listen_port. Use ``stop_recording`` to stop the recording.
:type video_file: str
:param format: Video format (default: h264)
:type format: str
"""
def recording_thread():
if video_file:
self.camera.start_recording(videofile, format=format)
self.camera.start_recording(video_file, format=format)
while True:
self.camera.wait_recording(60)
else:
@ -114,6 +146,8 @@ class CameraPiBackend(Backend):
def stop_recording(self):
""" Stops recording """
self.logger.info('Stopping camera recording')
try:

View file

@ -22,10 +22,30 @@ from .. import Backend
class HttpBackend(Backend):
""" Example interaction with the HTTP backend to make requests:
$ curl -XPOST -H 'Content-Type: application/json' -H "X-Token: your_token" \
-d '{"type":"request","target":"nodename","action":"tts.say","args": {"phrase":"This is a test"}}' \
http://localhost:8008/execute """
"""
The HTTP backend is a general-purpose web server that you can leverage:
* To execute Platypush commands via HTTP calls. Example::
curl -XPOST -H 'Content-Type: application/json' -H "X-Token: your_token" \\
-d '{
"type":"request",
"target":"nodename",
"action":"tts.say",
"args": {"phrase":"This is a test"}
}' \\
http://localhost:8008/execute
* To interact with your system (and control plugins and backends) through the Platypush web panel, by default available on your web root document. Any plugin that you have configured and available as a panel plugin will appear on the web panel as well as a tab.
* To display a fullscreen dashboard with your configured widgets, by default available under ``/dashboard``
Requires:
* **flask** (``pip install flask``)
* **redis** (``pip install redis``)
* **websockets** (``pip install websockets``)
"""
hidden_plugins = {
'assistant.google'
@ -34,6 +54,48 @@ class HttpBackend(Backend):
def __init__(self, port=8008, websocket_port=8009, disable_websocket=False,
redis_queue='platypush_flask_mq', token=None, dashboard={},
maps={}, **kwargs):
"""
:param port: Listen port for the web server (default: 8008)
:type port: int
:param websocket_port: Listen port for the websocket server (default: 8009)
:type websocket_port: int
:param disable_websocket: Disable the websocket interface (default: False)
:type disable_websocket: bool
:param redis_queue: Name of the Redis queue used to synchronize messages with the web server process (default: ``platypush_flask_mq``)
:type redis_queue: str
:param token: If set (recommended) any interaction with the web server needs to bear an ``X-Token: <token>`` header, or it will fail with a 403: Forbidden
:type token: str
:param dashboard: Set it if you want to use the dashboard service. It will contain the configuration for the widgets to be used (look under ``platypush/backend/http/templates/widgets/`` for the available widgets).
Example configuration::
dashboard:
background_image: https://site/image.png
widgets: # Each row of the dashboard will have 6 columns
calendar: # Calendar widget
columns: 6
music: # Music widget
columns: 3
date-time-weather: # Date, time and weather widget
columns: 3
image-carousel: # Image carousel
columns: 6
images_path: /static/resources/Dropbox/Photos/carousel # Path (relative to ``platypush/backend/http``) containing the carousel pictures
refresh_seconds: 15
rss-news: # RSS feeds widget
# Requires backend.http.poll to be enabled with some RSS sources and write them to sqlite db
columns: 6
limit: 25
db: "sqlite:////home/blacklight/.local/share/platypush/feeds/rss.db"
:type dashboard: dict
"""
super().__init__(**kwargs)
self.port = port
@ -54,6 +116,7 @@ class HttpBackend(Backend):
def stop(self):
""" Stop the web server """
self.logger.info('Received STOP event on HttpBackend')
if self.server_proc:
@ -62,6 +125,7 @@ class HttpBackend(Backend):
def notify_web_clients(self, event):
""" Notify all the connected web clients (over websocket) of a new event """
import websockets
async def send_event(websocket):
@ -77,6 +141,7 @@ class HttpBackend(Backend):
def redis_poll(self):
""" Polls for new messages on the internal Redis queue """
while not self.should_stop():
msg = self.redis.blpop(self.redis_queue)
msg = Message.build(json.loads(msg[1].decode('utf-8')))
@ -84,6 +149,7 @@ class HttpBackend(Backend):
def webserver(self):
""" Web server main process """
basedir = os.path.dirname(inspect.getfile(self.__class__))
template_dir = os.path.join(basedir, 'templates')
static_dir = os.path.join(basedir, 'static')
@ -92,6 +158,7 @@ class HttpBackend(Backend):
@app.route('/execute', methods=['POST'])
def execute():
""" Endpoint to execute commands """
args = json.loads(http_request.data.decode('utf-8'))
token = http_request.headers['X-Token'] if 'X-Token' in http_request.headers else None
if token != self.token: abort(401)
@ -111,6 +178,7 @@ class HttpBackend(Backend):
@app.route('/')
def index():
""" Route to the main web panel """
configured_plugins = Config.get_plugins()
enabled_plugins = {}
hidden_plugins = {}
@ -129,6 +197,7 @@ class HttpBackend(Backend):
@app.route('/widget/<widget>', methods=['POST'])
def widget_update(widget):
""" ``POST /widget/<widget_id>`` will update the specified widget_id on the dashboard with the specified key-values """
event = WidgetUpdateEvent(
widget=widget, **(json.loads(http_request.data.decode('utf-8'))))
@ -137,10 +206,12 @@ class HttpBackend(Backend):
@app.route('/static/<path>', methods=['GET'])
def static_path(path):
""" Static resources """
return send_from_directory(static_dir, filename)
@app.route('/dashboard', methods=['GET'])
def dashboard():
""" Route for the fullscreen dashboard """
return render_template('dashboard.html', config=self.dashboard, utils=HttpUtils,
token=self.token, websocket_port=self.websocket_port)
@ -219,6 +290,7 @@ class HttpBackend(Backend):
def websocket(self):
""" Websocket main server """
import websockets
async def register_websocket(websocket, path):

View file

@ -12,31 +12,47 @@ from platypush.message.request import Request
class HttpPollBackend(Backend):
"""
This backend will poll multiple HTTP endpoints/services and return events
the bus whenever something new happened. Example configuration:
the bus whenever something new happened. Supported types:
:class:`platypush.backend.http.request.JsonHttpRequest` (for polling updates on
a JSON endpoint), :class:`platypush.backend.http.request.rss.RssUpdates`
(for polling updates on an RSS feed). Example configuration::
backend.http.poll:
requests:
-
method: GET
type: platypush.backend.http.request.JsonHttpRequest
args:
url: https://host.com/api/v1/endpoint
headers:
Token: TOKEN
params:
updatedSince: 1m
timeout: 5 # Times out after 5 seconds (default)
poll_seconds: 60 # Check for updates on this endpoint every 60 seconds (default)
path: ${response['items']} # Path in the JSON to check for new items.
# Python expressions are supported.
# Note that 'response' identifies the JSON root.
# Default value: JSON root.
backend.http.poll:
requests:
-
# Poll for updates on a JSON endpoint
method: GET
type: platypush.backend.http.request.JsonHttpRequest
args:
url: https://host.com/api/v1/endpoint
headers:
Token: TOKEN
params:
updatedSince: 1m
timeout: 5 # Times out after 5 seconds (default)
poll_seconds: 60 # Check for updates on this endpoint every 60 seconds (default)
path: ${response['items']} # Path in the JSON to check for new items.
# Python expressions are supported.
# Note that 'response' identifies the JSON root.
# Default value: JSON root.
-
# Poll for updates on an RSS feed
type: platypush.backend.http.request.rss.RssUpdates
url: http://www.theguardian.com/rss/world
title: The Guardian - World News
poll_seconds: 120
max_entries: 10
Triggers: an update event for the relevant HTTP source if it contains new items. For example:
* :class:`platypush.message.event.http.rss.NewFeedEvent` if a feed contains new items
* :class:`platypush.message.event.http.HttpEvent` if a JSON endpoint contains new items
"""
def __init__(self, requests, *args, **kwargs):
"""
Params:
requests -- List/iterable of HttpRequest objects
:param requests: Configuration of the requests to make (see class description for examples)
:type requests: dict
"""
super().__init__(*args, **kwargs)
@ -67,9 +83,5 @@ class HttpPollBackend(Backend):
time.sleep(0.1) # Prevent a tight loop
def send_message(self, msg):
pass
# vim:sw=4:ts=4:et:

View file

@ -7,17 +7,38 @@ from platypush.message.event.path import PathCreateEvent, PathDeleteEvent, \
class InotifyBackend(Backend):
"""
(Linux only) This backend will listen for events on the filesystem (whether
a file/directory on a watch list is opened, modified, created, deleted,
closed or had its permissions changed) and will trigger a relevant event.
Triggers:
* :class:`platypush.message.event.path.PathCreateEvent` if a resource is created
* :class:`platypush.message.event.path.PathOpenEvent` if a resource is opened
* :class:`platypush.message.event.path.PathModifyEvent` if a resource is modified
* :class:`platypush.message.event.path.PathPermissionsChangeEvent` if the permissions of a resource are changed
* :class:`platypush.message.event.path.PathCloseEvent` if a resource is closed
* :class:`platypush.message.event.path.PathDeleteEvent` if a resource is removed
Requires:
* **inotify** (``pip install inotify``)
"""
inotify_watch = None
def __init__(self, watch_paths=[], **kwargs):
"""
:param watch_paths: Filesystem resources to watch for events
:type watch_paths: str
"""
super().__init__(**kwargs)
self.watch_paths = set(map(
lambda path: os.path.abspath(os.path.expanduser(path)),
watch_paths))
def send_message(self, msg):
pass
def _cleanup(self):
if not self.inotify_watch:
return

View file

@ -8,9 +8,26 @@ from .. import Backend
class KafkaBackend(Backend):
"""
Backend to interact with an Apache Kafka (https://kafka.apache.org/)
streaming platform, send and receive messages.
Requires:
* **kafka** (``pip install kafka-python``)
"""
_conn_retry_secs = 5
def __init__(self, server, topic, **kwargs):
def __init__(self, server, topic='platypush', **kwargs):
"""
:param server: Kafka server
:type server: str
:param topic: (Prefix) topic to listen to (default: platypush). The Platypush device_id (by default the hostname) will be appended to the topic (the real topic name will e.g. be "platypush.my_rpi")
:type topic: str
"""
super().__init__(**kwargs)
self.server = server

View file

@ -14,22 +14,28 @@ class MidiBackend(Backend):
This backend will listen for events from a MIDI device and post a
MidiMessageEvent whenever a new MIDI event happens.
It requires `rtmidi`, `pip install rtmidi`
Triggers:
* :class:`platypush.message.event.midi.MidiMessageEvent` when a new MIDI event is received
Requires:
* **rtmidi** (``pip install rtmidi``)
"""
def __init__(self, device_name=None, port_number=None,
midi_throttle_time=None, *args, **kwargs):
"""
Params:
device_name -- Name of the MIDI device.
*N.B.* either `device_name` or `port_number` must be set
port_number -- MIDI port number
*N.B.* either `device_name` or `port_number` must be set
midi_throttle_time -- If set, the MIDI events will be throttled -
max one per selected time frame (in seconds). Set this parameter
if you want to synchronize MIDI events with plugins that
normally operate with a lower throughput.
:param device_name: Name of the MIDI device. *N.B.* either `device_name` or `port_number` must be set
:type device_name: str
:param port_number: MIDI port number
:type port_number: int
:param midi_throttle_time: If set, the MIDI events will be throttled - max one per selected time frame (in seconds). Set this parameter if you want to synchronize MIDI events with plugins that normally operate with a lower throughput.
:type midi_throttle_time: int
"""
super().__init__(*args, **kwargs)
if (device_name and port_number is not None) or \

View file

@ -9,17 +9,26 @@ from platypush.message import Message
class MqttBackend(Backend):
"""
Backend that reads messages from a configured MQTT topic
(default: `platypush_bus_mq/<device_id>`) and posts them to the application bus.
Backend that reads messages from a configured MQTT topic (default:
``platypush_bus_mq/<device_id>``) and posts them to the application bus.
Requires:
* **paho-mqtt** (``pip install paho-mqtt``)
"""
def __init__(self, host, port=1883, topic='platypush_bus_mq', *args, **kwargs):
"""
Params:
host -- MQTT broker host
port -- MQTT broker port (default: 1883)
topic -- Topic to read messages from (default: platypush_bus_mq/<device_id>)
:param host: MQTT broker host
:type host: str
:param port: MQTT broker port (default: 1883)
:type port: int
:param topic: Topic to read messages from (default: ``platypush_bus_mq/<device_id>``)
:type topic: str
"""
super().__init__(*args, **kwargs)
self.host = host

View file

@ -8,7 +8,28 @@ from platypush.message.event.music import MusicPlayEvent, MusicPauseEvent, \
class MusicMpdBackend(Backend):
"""
This backend listens for events on a MPD/Mopidy music server.
Triggers:
* :class:`platypush.message.event.music.MusicPlayEvent` if the playback state changed to play
* :class:`platypush.message.event.music.MusicPauseEvent` if the playback state changed to pause
* :class:`platypush.message.event.music.MusicStopEvent` if the playback state changed to stop
* :class:`platypush.message.event.music.NewPlayingTrackEvent` if a new track is being played
* :class:`platypush.message.event.music.PlaylistChangeEvent` if the main playlist has changed
Requires:
* **python-mpd2** (``pip install python-mpd2``)
* The :mod:`platypush.plugins.music.mpd` plugin to be configured
"""
def __init__(self, server='localhost', port=6600, poll_seconds=3, **kwargs):
"""
:param poll_seconds: Interval between queries to the server (default: 3 seconds)
:type poll_seconds: float
"""
super().__init__(**kwargs)
self.server = server
@ -16,10 +37,6 @@ class MusicMpdBackend(Backend):
self.poll_seconds = poll_seconds
def send_message(self, msg):
pass
def run(self):
super().run()

View file

@ -11,7 +11,33 @@ from .. import Backend
class PushbulletBackend(Backend):
def __init__(self, token, device, **kwargs):
"""
This backend will listen for events on a Pushbullet (https://pushbullet.com)
channel and propagate them to the bus. This backend is quite useful if you
want to synchronize events and actions with your mobile phone (through the
Pushbullet app and/or through Tasker), synchronize clipboards, send pictures
and files to other devices etc. You can also wrap Platypush messages as JSON
into a push body to execute them.
Triggers:
* :class:`platypush.message.event.pushbullet` if a new push is received
Requires:
* **requests** (``pip install requests``)
* **websocket-client** (``pip install websocket-client``)
"""
def __init__(self, token, device='Platypush', **kwargs):
"""
:param token: Your Pushbullet API token, see https://docs.pushbullet.com/#authentication
:type token: str
:param device: Name of the virtual device for Platypush (default: Platypush)
:type device: str
"""
super().__init__(**kwargs)
self.token = token

View file

@ -8,20 +8,25 @@ from platypush.message import Message
class RedisBackend(Backend):
"""
Backend that reads messages from a configured Redis queue
(default: `platypush_bus_mq`) and posts them to the application bus.
Very useful when you have plugin whose code is executed in another process
Backend that reads messages from a configured Redis queue (default:
``platypush_bus_mq``) and posts them to the application bus. Very
useful when you have plugin whose code is executed in another process
and can't post events or requests to the application bus.
Requires:
* **redis** (``pip install redis``)
"""
def __init__(self, queue='platypush_bus_mq', redis_args={}, *args, **kwargs):
"""
Params:
queue -- Queue to poll for new messages
redis_args -- Arguments that will be passed to the redis-py
constructor (e.g. host, port, password),
see http://redis-py.readthedocs.io/en/latest/
:param queue: Queue name to listen on (default: ``platypush_bus_mq``)
:type queue: str
:param redis_args: Arguments that will be passed to the redis-py constructor (e.g. host, port, password), see http://redis-py.readthedocs.io/en/latest/
:type redis_args: dict
"""
super().__init__(*args, **kwargs)
self.queue = queue

View file

@ -11,22 +11,27 @@ from platypush.message.event.scard import SmartCardDetectedEvent, SmartCardRemov
class ScardBackend(Backend):
"""
Generic backend to read smart cards and trigger SmartCardDetectedEvent
messages with the card ATR whenever a card is detected. It requires
pyscard https://pypi.org/project/pyscard/
Generic backend to read smart cards and NFC tags and trigger an event
whenever a device is detected.
Extend this backend to implement more advanced communication with
custom smart cards.
Extend this backend to implement more advanced communication with custom
smart cards.
Triggers:
* :class:`platypush.message.event.scard.SmartCardDetectedEvent` when a smart card is detected
* :class:`platypush.message.event.scard.SmartCardRemovedEvent` when a smart card is removed
Requires:
* **pyscard** (``pip install pyscard``)
"""
def __init__(self, atr=None, *args, **kwargs):
"""
Params:
atr -- If set, the backend will trigger events only for card(s)
with the specified ATR(s). It can be either an ATR string
(space-separated hex octects) or a list of ATR strings.
Default: none (any card will be detected)
:param atr: If set, the backend will trigger events only for card(s) with the specified ATR(s). It can be either an ATR string (space-separated hex octects) or a list of ATR strings. Default: none (any card will be detected)
"""
super().__init__(*args, **kwargs)
self.ATRs = []

View file

@ -6,25 +6,28 @@ from platypush.message.event.sensor import SensorDataChangeEvent, \
class SensorBackend(Backend):
"""
Abstract backend for polling sensors.
Triggers:
* :class:`platypush.message.event.sensor.SensorDataChangeEvent` if the measurements of a sensor have changed
* :class:`platypush.message.event.sensor.SensorDataAboveThresholdEvent` if the measurements of a sensor have gone above a configured threshold
* :class:`platypush.message.event.sensor.SensorDataBelowThresholdEvent` if the measurements of a sensor have gone below a configured threshold
"""
def __init__(self, thresholds=None, poll_seconds=None, *args, **kwargs):
"""
Params:
-- thresholds: Thresholds can be either a scalr value or a dictionary.
:param thresholds: Thresholds can be either a scalar value or a dictionary (e.g. ``{"temperature": 20.0}``). Sensor threshold events will be fired when measurements get above or below these values. Set it as a scalar if your get_measurement() code returns a scalar, as a dictionary if it returns a dictionary of values.
If set, SensorDataAboveThresholdEvent and SensorDataBelowThresholdEvent
events will be triggered whenever the measurement goes above or
below that value.
For instance, if your sensor code returns both humidity and
temperature in a format like ``{'humidity':60.0, 'temperature': 25.0}``,
you'll want to set up a threshold on temperature with a syntax like
``{'temperature':20.0}`` to trigger events when the temperature goes
above/below 20 degrees.
Set it as a scalar if your get_measurement() code returns a scalar,
as a dictionary if it returns a dictionary of values.
For instance, if your sensor code returns both humidity and
temperature in a format like {'humidity':60.0, 'temperature': 25.0},
you'll want to set up a threshold on temperature with a syntax like
{'temperature':20.0}
-- poll_seconds: If set, the thread will wait for the specificed
number of seconds between a read and the next one.
:param poll_seconds: If set, the thread will wait for the specificed number of seconds between a read and the next one.
:type poll_seconds: float
"""
super().__init__(**kwargs)
@ -34,6 +37,7 @@ class SensorBackend(Backend):
self.poll_seconds = poll_seconds
def get_measurement(self):
""" To be implemented in the derived classes """
raise NotImplementedError('To be implemented in a derived class')
def run(self):