diff --git a/docs/source/backends.rst b/docs/source/backends.rst new file mode 100644 index 0000000000..9beee03fb9 --- /dev/null +++ b/docs/source/backends.rst @@ -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 + diff --git a/docs/source/index.rst b/docs/source/index.rst index 9800dafb69..457361856f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,6 +5,7 @@ Platypush :maxdepth: 3 :caption: Contents: + backends plugins Indices and tables diff --git a/docs/source/platypush/backend.rst b/docs/source/platypush/backend.rst new file mode 100644 index 0000000000..1481f07f8c --- /dev/null +++ b/docs/source/platypush/backend.rst @@ -0,0 +1,6 @@ +``platypush.backend`` +===================== + +.. automodule:: platypush.backend + :members: + diff --git a/docs/source/platypush/backend/assistant.google.pushtotalk.rst b/docs/source/platypush/backend/assistant.google.pushtotalk.rst new file mode 100644 index 0000000000..21634b90d6 --- /dev/null +++ b/docs/source/platypush/backend/assistant.google.pushtotalk.rst @@ -0,0 +1,6 @@ +``platypush.backend.assistant.google.pushtotalk`` +================================================= + +.. automodule:: platypush.backend.assistant.google.pushtotalk + :members: + diff --git a/docs/source/platypush/backend/assistant.google.rst b/docs/source/platypush/backend/assistant.google.rst new file mode 100644 index 0000000000..96e4c74b05 --- /dev/null +++ b/docs/source/platypush/backend/assistant.google.rst @@ -0,0 +1,6 @@ +``platypush.backend.assistant.google`` +====================================== + +.. automodule:: platypush.backend.assistant.google + :members: + diff --git a/docs/source/platypush/backend/assistant.snowboy.rst b/docs/source/platypush/backend/assistant.snowboy.rst new file mode 100644 index 0000000000..e130c6f4a0 --- /dev/null +++ b/docs/source/platypush/backend/assistant.snowboy.rst @@ -0,0 +1,6 @@ +``platypush.backend.assistant.snowboy`` +======================================= + +.. automodule:: platypush.backend.assistant.snowboy + :members: + diff --git a/docs/source/platypush/backend/button.flic.rst b/docs/source/platypush/backend/button.flic.rst new file mode 100644 index 0000000000..a9a8c98915 --- /dev/null +++ b/docs/source/platypush/backend/button.flic.rst @@ -0,0 +1,6 @@ +``platypush.backend.button.flic`` +================================= + +.. automodule:: platypush.backend.button.flic + :members: + diff --git a/docs/source/platypush/backend/camera.pi.rst b/docs/source/platypush/backend/camera.pi.rst new file mode 100644 index 0000000000..f8073b52c9 --- /dev/null +++ b/docs/source/platypush/backend/camera.pi.rst @@ -0,0 +1,6 @@ +``platypush.backend.camera.pi`` +=============================== + +.. automodule:: platypush.backend.camera.pi + :members: + diff --git a/docs/source/platypush/backend/http.poll.rst b/docs/source/platypush/backend/http.poll.rst new file mode 100644 index 0000000000..36658ba184 --- /dev/null +++ b/docs/source/platypush/backend/http.poll.rst @@ -0,0 +1,6 @@ +``platypush.backend.http.poll`` +=============================== + +.. automodule:: platypush.backend.http.poll + :members: + diff --git a/docs/source/platypush/backend/http.rst b/docs/source/platypush/backend/http.rst new file mode 100644 index 0000000000..f84ae7784b --- /dev/null +++ b/docs/source/platypush/backend/http.rst @@ -0,0 +1,6 @@ +``platypush.backend.http`` +========================== + +.. automodule:: platypush.backend.http + :members: + diff --git a/docs/source/platypush/backend/inotify.rst b/docs/source/platypush/backend/inotify.rst new file mode 100644 index 0000000000..c432999fc2 --- /dev/null +++ b/docs/source/platypush/backend/inotify.rst @@ -0,0 +1,6 @@ +``platypush.backend.inotify`` +============================= + +.. automodule:: platypush.backend.inotify + :members: + diff --git a/docs/source/platypush/backend/kafka.rst b/docs/source/platypush/backend/kafka.rst new file mode 100644 index 0000000000..a2cb7d7177 --- /dev/null +++ b/docs/source/platypush/backend/kafka.rst @@ -0,0 +1,6 @@ +``platypush.backend.kafka`` +=========================== + +.. automodule:: platypush.backend.kafka + :members: + diff --git a/docs/source/platypush/backend/midi.rst b/docs/source/platypush/backend/midi.rst new file mode 100644 index 0000000000..f53029326f --- /dev/null +++ b/docs/source/platypush/backend/midi.rst @@ -0,0 +1,6 @@ +``platypush.backend.midi`` +========================== + +.. automodule:: platypush.backend.midi + :members: + diff --git a/docs/source/platypush/backend/mqtt.rst b/docs/source/platypush/backend/mqtt.rst new file mode 100644 index 0000000000..ccdee80207 --- /dev/null +++ b/docs/source/platypush/backend/mqtt.rst @@ -0,0 +1,6 @@ +``platypush.backend.mqtt`` +========================== + +.. automodule:: platypush.backend.mqtt + :members: + diff --git a/docs/source/platypush/backend/music.mpd.rst b/docs/source/platypush/backend/music.mpd.rst new file mode 100644 index 0000000000..b5fbfc915e --- /dev/null +++ b/docs/source/platypush/backend/music.mpd.rst @@ -0,0 +1,6 @@ +``platypush.backend.music.mpd`` +=============================== + +.. automodule:: platypush.backend.music.mpd + :members: + diff --git a/docs/source/platypush/backend/pushbullet.rst b/docs/source/platypush/backend/pushbullet.rst new file mode 100644 index 0000000000..4d5269a97d --- /dev/null +++ b/docs/source/platypush/backend/pushbullet.rst @@ -0,0 +1,6 @@ +``platypush.backend.pushbullet`` +================================ + +.. automodule:: platypush.backend.pushbullet + :members: + diff --git a/docs/source/platypush/backend/redis.rst b/docs/source/platypush/backend/redis.rst new file mode 100644 index 0000000000..6554bd455b --- /dev/null +++ b/docs/source/platypush/backend/redis.rst @@ -0,0 +1,6 @@ +``platypush.backend.redis`` +=========================== + +.. automodule:: platypush.backend.redis + :members: + diff --git a/docs/source/platypush/backend/scard.rst b/docs/source/platypush/backend/scard.rst new file mode 100644 index 0000000000..f95b8a0494 --- /dev/null +++ b/docs/source/platypush/backend/scard.rst @@ -0,0 +1,6 @@ +``platypush.backend.scard`` +=========================== + +.. automodule:: platypush.backend.scard + :members: + diff --git a/docs/source/platypush/backend/sensor.rst b/docs/source/platypush/backend/sensor.rst new file mode 100644 index 0000000000..b826b93290 --- /dev/null +++ b/docs/source/platypush/backend/sensor.rst @@ -0,0 +1,7 @@ +``platypush.backend.sensor`` +============================ + +.. automodule:: platypush.backend.sensor + :members: + + diff --git a/platypush/backend/__init__.py b/platypush/backend/__init__.py index 4c9a670aeb..dca6575d8b 100644 --- a/platypush/backend/__init__.py +++ b/platypush/backend/__init__.py @@ -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') diff --git a/platypush/backend/assistant/google/__init__.py b/platypush/backend/assistant/google/__init__.py index 1a4c0bdca3..81cc541255 100644 --- a/platypush/backend/assistant/google/__init__.py +++ b/platypush/backend/assistant/google/__init__.py @@ -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() diff --git a/platypush/backend/assistant/google/pushtotalk.py b/platypush/backend/assistant/google/pushtotalk.py index b88eeb12ec..800a507aaf 100644 --- a/platypush/backend/assistant/google/pushtotalk.py +++ b/platypush/backend/assistant/google/pushtotalk.py @@ -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: diff --git a/platypush/backend/assistant/snowboy/__init__.py b/platypush/backend/assistant/snowboy/__init__.py index ceab4ce417..79286ec369 100644 --- a/platypush/backend/assistant/snowboy/__init__.py +++ b/platypush/backend/assistant/snowboy/__init__.py @@ -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)) diff --git a/platypush/backend/button/flic/__init__.py b/platypush/backend/button/flic/__init__.py index 617a1963e7..bf9f1f670b 100644 --- a/platypush/backend/button/flic/__init__.py +++ b/platypush/backend/button/flic/__init__.py @@ -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() diff --git a/platypush/backend/camera/pi.py b/platypush/backend/camera/pi.py index c2fc934601..3943af0772 100644 --- a/platypush/backend/camera/pi.py +++ b/platypush/backend/camera/pi.py @@ -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: diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index f5e53320c7..fed6f46fd7 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -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: `` 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/', methods=['POST']) def widget_update(widget): + """ ``POST /widget/`` 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/', 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): diff --git a/platypush/backend/http/poll/__init__.py b/platypush/backend/http/poll/__init__.py index 934bc14fb4..52dccea276 100644 --- a/platypush/backend/http/poll/__init__.py +++ b/platypush/backend/http/poll/__init__.py @@ -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: diff --git a/platypush/backend/inotify/__init__.py b/platypush/backend/inotify/__init__.py index 8738308f59..8dae146b1f 100644 --- a/platypush/backend/inotify/__init__.py +++ b/platypush/backend/inotify/__init__.py @@ -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 diff --git a/platypush/backend/kafka/__init__.py b/platypush/backend/kafka/__init__.py index 9ed003e8cb..70487f2ea6 100644 --- a/platypush/backend/kafka/__init__.py +++ b/platypush/backend/kafka/__init__.py @@ -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 diff --git a/platypush/backend/midi.py b/platypush/backend/midi.py index 83fb21dc55..43f40921ef 100644 --- a/platypush/backend/midi.py +++ b/platypush/backend/midi.py @@ -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 \ diff --git a/platypush/backend/mqtt.py b/platypush/backend/mqtt.py index e875e5e5f4..d31cec9c14 100644 --- a/platypush/backend/mqtt.py +++ b/platypush/backend/mqtt.py @@ -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/`) and posts them to the application bus. + Backend that reads messages from a configured MQTT topic (default: + ``platypush_bus_mq/``) 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/) + :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/``) + :type topic: str """ + super().__init__(*args, **kwargs) self.host = host diff --git a/platypush/backend/music/mpd/__init__.py b/platypush/backend/music/mpd/__init__.py index 95d6b82638..6d42c533ec 100644 --- a/platypush/backend/music/mpd/__init__.py +++ b/platypush/backend/music/mpd/__init__.py @@ -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() diff --git a/platypush/backend/pushbullet/__init__.py b/platypush/backend/pushbullet/__init__.py index 86f03f9a14..4f7b7c6343 100644 --- a/platypush/backend/pushbullet/__init__.py +++ b/platypush/backend/pushbullet/__init__.py @@ -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 diff --git a/platypush/backend/redis.py b/platypush/backend/redis.py index a78174f1a0..a297a8d97b 100644 --- a/platypush/backend/redis.py +++ b/platypush/backend/redis.py @@ -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 diff --git a/platypush/backend/scard/__init__.py b/platypush/backend/scard/__init__.py index ebd766685e..2dcf231ed0 100644 --- a/platypush/backend/scard/__init__.py +++ b/platypush/backend/scard/__init__.py @@ -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 = [] diff --git a/platypush/backend/sensor/__init__.py b/platypush/backend/sensor/__init__.py index 860eb769c4..db1d87536f 100644 --- a/platypush/backend/sensor/__init__.py +++ b/platypush/backend/sensor/__init__.py @@ -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):