Adding more plugins documentation

This commit is contained in:
Fabio Manganiello 2018-06-25 00:49:45 +02:00
parent 135212efcb
commit ad1c87b2be
22 changed files with 1030 additions and 49 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
``platypush.plugins.gpio.zeroborg``
===================================
.. automodule:: platypush.plugins.gpio.zeroborg
:members:

View file

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

View file

@ -0,0 +1,6 @@
``platypush.plugins.light.hue``
===============================
.. automodule:: platypush.plugins.light.hue
:members:

View file

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

View file

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

View file

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

View file

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

View file

@ -16,4 +16,14 @@ Plugins
platypush/plugins/google.mail.rst
platypush/plugins/google.maps.rst
platypush/plugins/gpio.rst
platypush/plugins/gpio.sensor.rst
platypush/plugins/gpio.sensor.distance.rst
platypush/plugins/gpio.sensor.mcp3008.rst
platypush/plugins/gpio.zeroborg.rst
platypush/plugins/http.request.rst
platypush/plugins/light.rst
platypush/plugins/light.hue.rst
platypush/plugins/midi.rst
platypush/plugins/mqtt.rst
platypush/plugins/music.mpd.rst

View file

@ -2,13 +2,36 @@ from platypush.plugins import Plugin
class GpioSensorPlugin(Plugin):
"""
GPIO sensor abstract plugin. Any plugin that interacts with sensor via GPIO
should implement this class (and the get_measurement() method)
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_measurement(self, *args, **kwargs):
"""
Implemented by the subclasses.
:returns: Either a raw scalar:
``output = 273.16``
or a name-value dictionary with the values that have been read::
output = {
"temperature": 21.5,
"humidity": 41.0
}
"""
raise NotImplementedError('get_measurement should be implemented in a derived class')
def get_data(self, *args, **kwargs):
"""
Alias for ``get_measurement``
"""
return self.get_measurement(*args, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -1,14 +1,30 @@
import threading
import time
import RPi.GPIO as gpio
from platypush.message.response import Response
from platypush.plugins.gpio.sensor import GpioSensorPlugin
class GpioSensorDistancePlugin(GpioSensorPlugin):
"""
You can use this plugin to interact with a distance sensor on your Raspberry
Pi (tested with a HC-SR04 ultrasound sensor).
Requires:
* ``RPi.GPIO`` (``pip install RPi.GPIO``)
"""
def __init__(self, trigger_pin, echo_pin, *args, **kwargs):
"""
:param trigger_pin: GPIO PIN where you connected your sensor trigger PIN (the one that triggers the sensor to perform a measurement).
:type trigger_pin: int
:param echo_pin: GPIO PIN where you connected your sensor echo PIN (the one that will listen for the signal to bounce back and therefore trigger the distance calculation).
:type echo_pin: int
"""
import RPi.GPIO as gpio
super().__init__(*args, **kwargs)
self.trigger_pin = trigger_pin
@ -24,6 +40,13 @@ class GpioSensorDistancePlugin(GpioSensorPlugin):
def get_measurement(self):
"""
Extends :func:`.GpioSensorPlugin.get_measurement`
:returns: Distance measurement as a scalar (in mm):
"""
import RPi.GPIO as gpio
gpio.setmode(gpio.BCM)
gpio.setup(self.trigger_pin, gpio.OUT)
gpio.setup(self.echo_pin, gpio.IN)

View file

@ -1,9 +1,6 @@
import enum
import time
import Adafruit_GPIO.SPI as SPI
import Adafruit_MCP3008
from platypush.plugins.gpio.sensor import GpioSensorPlugin
from platypush.message.response import Response
@ -15,15 +12,78 @@ class MCP3008Mode(enum.Enum):
class GpioSensorMcp3008Plugin(GpioSensorPlugin):
"""
Plugin to read analog sensor values from an MCP3008 chipset,
see https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008
Requires adafruit-mcp3008 Python package
Plugin to read analog sensor values from an MCP3008 chipset. The MCP3008
chipset is a circuit that allows you to read measuremnts from multiple
analog sources (e.g. sensors) and multiplex them to a digital device like a
Raspberry Pi or a regular laptop. See
https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008
for more info.
Requires:
* ``adafruit-mcp3008`` (``pip install adafruit-mcp3008``)
"""
N_CHANNELS = 8
def __init__(self, CLK=None, MISO=None, MOSI=None, CS=None, spi_port=None,
spi_device=None, channels=None, Vdd=3.3, *args, **kwargs):
"""
The MCP3008 can be connected in two modes:
* Hardware SPI mode: advised if you have enough GPIO pins available
(and slightly faster)
* Software SPI mode: useful if you don't have all the required GPIO
PINs for hardware SPI available. Slightly slower, as the conversion
is done via software, but still relatively performant.
See
https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008#wiring
for info
:param CLK: (software SPI mode) CLK GPIO PIN
:type CLK: int
:param MISO: (software SPI mode) MISO GPIO PIN
:type MISO: int
:param MOSI: (software SPI mode) MOSI GPIO PIN
:type MOSI: int
:param CS: (software SPI mode) CS GPIO PIN
:type CS: int
:param spi_port: (hardware SPI mode) SPI port
:type spi_port: int
:param spi_device: (hardware SPI mode) SPI device name
:type spi_device: str
:param channels: name-value mapping between MCP3008 output PINs and sensor names. This mapping will be used when you get values through :func:`.get_measurement()`.
Example::
channels = {
"0": {
"name": "temperature",
"conv_function": 'round(x*100.0, 2)' # T = Vout / (10 [mV/C])
},
"1": {
"name": "light", # ALS-PT9
"conv_function": 'round(x*1000.0, 6)' # ALS-PT9 has a 10 kOhm resistor
}
}
Note that you can also pass a conversion function as
``conv_function`` that will convert the output voltage to whichever
human-readable value you wish. In the case above I connected a
simple temperature sensor to the channel 0 and a simple ALS-PT9
light sensor to the channel 1, and passed the appropriate conversion
functions to convert from voltage to, respectively, temperature in
Celsius degrees and light intensity in lumen. Note that we reference
the current voltage as ``x`` in ``conv_function``.
:type channels: dict
:param Vdd: Input voltage provided to the circuit (default: 3.3V, Raspberry Pi default power source)
:type Vdd: float
"""
super().__init__(*args, **kwargs)
if CLK and MISO and MOSI and CS:
@ -47,6 +107,9 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
def _get_mcp(self):
import Adafruit_GPIO.SPI as SPI
import Adafruit_MCP3008
if self.mode == MCP3008Mode.SOFTWARE:
self.mcp = Adafruit_MCP3008.MCP3008(clk=self.CLK, cs=self.CS,
miso=self.MISO, mosi=self.MOSI)
@ -63,6 +126,27 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
def get_measurement(self):
"""
Returns a measurement from the sensors connected to the MCP3008 device.
If channels were passed to the configuration, the appropriate sensor names
will be used and the voltage will be converted through the appropriate
conversion function. Example::
output = {
"temperature": 21.0, # Celsius
"humidity": 45.1 # %
}
Otherwise, the output dictionary will contain the channel numbers as key
and the row voltage (between 0 and 255) will be returned. Example::
output = {
"0": 145,
"1": 130
}
"""
mcp = self._get_mcp()
values = {}

View file

@ -18,12 +18,65 @@ class Direction(enum.Enum):
class GpioZeroborgPlugin(Plugin):
"""
ZeroBorg plugin. It allows you to control a ZeroBorg
(https://www.piborg.org/motor-control-1135/zeroborg) motor controller and
infrared sensor circuitry for Raspberry Pi
"""
_drive_thread = None
_can_run = False
_direction = None
def __init__(self, directions = {}, *args, **kwargs):
"""
:param directions: Configuration for the motor directions. A direction is basically a configuration of the power delivered to each motor to allow whichever object you're controlling (wheels, robotic arms etc.) to move in a certain direction. In my experience the ZeroBorg always needs a bit of calibration, depending on factory defaults and the mechanical properties of the load it controls.
Example configuration that I use to control a simple 4WD robot::
directions = {
"up": {
"motor_1_power": 0.4821428571428572,
"motor_2_power": 0.4821428571428572,
"motor_3_power": -0.6707142857142858,
"motor_4_power": -0.6707142857142858
},
"down": {
"motor_1_power": -0.4821428571428572,
"motor_2_power": -0.4821428571428572,
"motor_3_power": 0.825,
"motor_4_power": 0.825
},
"left": {
"motor_1_power": -0.1392857142857143,
"motor_2_power": -0.1392857142857143,
"motor_3_power": -1.0553571428571429,
"motor_4_power": -1.0553571428571429
},
"right": {
"motor_1_power": 1.0017857142857143,
"motor_2_power": 1.0017857142857143,
"motor_3_power": 0.6214285714285713,
"motor_4_power": 0.6214285714285713
},
"auto": {
"sensors": [
{
"plugin": "gpio.sensor.distance",
"threshold": 400.0,
"timeout": 2.0,
"above_threshold_direction": "up",
"below_threshold_direction": "left"
}
]
}
}
Note that the special direction "auto" can contain a configuration that allows your device to move autonomously based on the inputs it gets from some sensors. In this case, I set the sensors configuration (a list) to periodically poll a GPIO-based ultrasound distance sensor plugin. ``timeout`` says after how long a poll attempt should fail. The plugin package is specified through ``plugin`` (``gpio.sensor.distance``) in this case, note that the plugin must be configured as well in order to work). The ``threshold`` value says around which value your logic should trigger. In this case, threshold=400 (40 cm). When the distance value is above that threshold (``above_threshold_direction``), then go "up" (no obstacles ahead). Otherwise (``below_threshold_direction``), turn "left" (avoid the obstacle).
:type directions: dict
"""
import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
super().__init__(*args, **kwargs)
@ -75,6 +128,16 @@ class GpioZeroborgPlugin(Plugin):
def drive(self, direction):
"""
Drive the motors in a certain direction.
:param direction: Direction name (note: it must be a configured direction). Special directions:
* ``auto`` - Enter automatic drive mode
* ``auto_toggle`` - Toggle automatic drive mode (on or off)
* ``stop`` - Turn off the motors
"""
prev_direction = self._direction
self._can_run = True
@ -125,6 +188,10 @@ class GpioZeroborgPlugin(Plugin):
def stop(self):
"""
Turns off the motors
"""
self._can_run = False
if self._drive_thread and threading.get_ident() != self._drive_thread.ident:
self._drive_thread.join()

View file

@ -5,7 +5,41 @@ from platypush.message.response import Response
from platypush.plugins import Plugin
class HttpRequestPlugin(Plugin):
""" Plugin for executing custom HTTP requests """
"""
Plugin for executing custom HTTP requests.
Requires:
* **requests** (``pip install requests``)
Some example usages::
# Execute a GET request on a JSON endpoint
{
"type": "request",
"action": "http.request.get",
"args": {
"url": "http://remote-host/api/v1/entity",
"params": {
"start": "2000-01-01"
}
}
}
# Execute an action on another Platypush host through HTTP interface
{
"type": "request",
"action": "http.request.post",
"args": {
"url": "http://remote-host:8008/execute",
"json": {
"type": "request",
"target": "remote-host",
"action": "music.mpd.play"
}
}
}
"""
def _exec(self, method, url, output='text', **kwargs):
""" Available output types: text (default), json, binary """
@ -21,26 +55,86 @@ class HttpRequestPlugin(Plugin):
def get(self, url, **kwargs):
"""
Perform a GET request
:param url: Target URL
:type url: str
:param kwargs: Additional arguments that will be transparently provided to the ``requests`` object, including but not limited to query params, data, JSON, headers etc. (see http://docs.python-requests.org/en/master/user/quickstart/#make-a-request)
:type kwargs: dict
"""
return self._exec(method='get', url=url, **kwargs)
def post(self, url, **kwargs):
"""
Perform a POST request
:param url: Target URL
:type url: str
:param kwargs: Additional arguments that will be transparently provided to the ``requests`` object, including but not limited to query params, data, JSON, headers etc. (see http://docs.python-requests.org/en/master/user/quickstart/#make-a-request)
:type kwargs: dict
"""
return self._exec(method='post', url=url, **kwargs)
def head(self, url, **kwargs):
"""
Perform an HTTP HEAD request
:param url: Target URL
:type url: str
:param kwargs: Additional arguments that will be transparently provided to the ``requests`` object, including but not limited to query params, data, JSON, headers etc. (see http://docs.python-requests.org/en/master/user/quickstart/#make-a-request)
:type kwargs: dict
"""
return self._exec(method='head', url=url, **kwargs)
def put(self, url, **kwargs):
"""
Perform a PUT request
:param url: Target URL
:type url: str
:param kwargs: Additional arguments that will be transparently provided to the ``requests`` object, including but not limited to query params, data, JSON, headers etc. (see http://docs.python-requests.org/en/master/user/quickstart/#make-a-request)
:type kwargs: dict
"""
return self._exec(method='put', url=url, **kwargs)
def delete(self, url, **kwargs):
"""
Perform a DELETE request
:param url: Target URL
:type url: str
:param kwargs: Additional arguments that will be transparently provided to the ``requests`` object, including but not limited to query params, data, JSON, headers etc. (see http://docs.python-requests.org/en/master/user/quickstart/#make-a-request)
:type kwargs: dict
"""
return self._exec(method='delete', url=url, **kwargs)
def options(self, url, **kwargs):
"""
Perform an HTTP OPTIONS request
:param url: Target URL
:type url: str
:param kwargs: Additional arguments that will be transparently provided to the ``requests`` object, including but not limited to query params, data, JSON, headers etc. (see http://docs.python-requests.org/en/master/user/quickstart/#make-a-request)
:type kwargs: dict
"""
return self._exec(method='options', url=url, **kwargs)

View file

@ -6,7 +6,30 @@ from platypush.message.response import Response
from .. import Plugin
class LastfmPlugin(Plugin):
"""
Plugin to interact with your Last.FM (https://last.fm) account, update your
current track and your scrobbles.
Requires:
* **pylast** (``pip install pylast``)
"""
def __init__(self, api_key, api_secret, username, password):
"""
:param api_key: Last.FM API key, see https://www.last.fm/api
:type api_key: str
:param api_secret: Last.FM API secret, see https://www.last.fm/api
:type api_key: str
:param username: Last.FM username
:type api_key: str
:param password: Last.FM password, used to sign the requests
:type api_key: str
"""
self.api_key = api_key
self.api_secret = api_secret
self.username = username
@ -20,6 +43,17 @@ class LastfmPlugin(Plugin):
def scrobble(self, artist, title, album=None, **kwargs):
"""
Scrobble a track to Last.FM
:param artist: Artist
:type artist: str
:param title: Title
:type title: str
:param album: Album (optional)
:type album: str
"""
self.lastfm.scrobble(
artist = artist,
title = title,
@ -31,6 +65,17 @@ class LastfmPlugin(Plugin):
def update_now_playing(self, artist, title, album=None, **kwargs):
"""
Update the currently playing track
:param artist: Artist
:type artist: str
:param title: Title
:type title: str
:param album: Album (optional)
:type album: str
"""
self.lastfm.update_now_playing(
artist = artist,
title = title,

View file

@ -1,16 +1,24 @@
from .. import Plugin
class LightPlugin(Plugin):
"""
Abstract plugin to interface your logic with lights/bulbs.
"""
def on(self):
""" Turn the light on """
raise NotImplementedError()
def off(self):
""" Turn the light off """
raise NotImplementedError()
def toggle(self):
""" Toggle the light status (on/off) """
raise NotImplementedError()
def status(self):
""" Get the light status """
raise NotImplementedError()

View file

@ -12,7 +12,13 @@ from platypush.message.response import Response
from .. import LightPlugin
class LightHuePlugin(LightPlugin):
""" Philips Hue lights plugin """
"""
Philips Hue lights plugin.
Requires:
* **phue** (``pip install phue``)
"""
MAX_BRI = 255
MAX_SAT = 255
@ -31,11 +37,14 @@ class LightHuePlugin(LightPlugin):
def __init__(self, bridge, lights=None, groups=None):
"""
Constructor
Params:
bridge -- Bridge address or hostname
lights -- Lights to be controlled (default: all)
groups -- Groups to be controlled (default: all)
:param bridge: Bridge address or hostname
:type bridge: str
:param lights: Default lights to be controlled (default: all)
:type lights: list[str]
:param groups Default groups to be controlled (default: all)
:type groups: list[str]
"""
super().__init__()
@ -66,6 +75,14 @@ class LightHuePlugin(LightPlugin):
self.lights.extend([l.name for l in g.lights])
def connect(self):
"""
Connect to the configured Hue bridge. If the device hasn't been paired
yet, uncomment the ``.connect()`` and ``.get_api()`` lines and retry
after clicking the pairing button on your bridge.
:todo: Support for dynamic retry and better user interaction in case of bridge pairing neeeded.
"""
# Lazy init
if not self.bridge:
self.bridge = Bridge(self.bridge_address)
@ -83,14 +100,127 @@ class LightHuePlugin(LightPlugin):
def get_scenes(self):
"""
Get the available scenes on the devices.
:returns: The scenes configured on the bridge.
Example output::
output = {
"scene-id-1": {
"name": "Scene 1",
"lights": [
"1",
"3"
],
"owner": "owner-id",
"recycle": true,
"locked": false,
"appdata": {},
"picture": "",
"lastupdated": "2018-06-01T00:00:00",
"version": 1
},
"scene-id-2": {
# ...
}
}
"""
return Response(output=self.bridge.get_scene())
def get_lights(self):
"""
Get the configured lights.
:returns: List of available lights as id->dict.
Example::
output = {
"1": {
"state": {
"on": true,
"bri": 254,
"hue": 1532,
"sat": 215,
"effect": "none",
"xy": [
0.6163,
0.3403
],
"ct": 153,
"alert": "none",
"colormode": "hs",
"reachable": true
},
"type": "Extended color light",
"name": "Lightbulb 1",
"modelid": "LCT001",
"manufacturername": "Philips",
"uniqueid": "00:11:22:33:44:55:66:77-88",
"swversion": "5.105.0.21169"
},
"2": {
# ...
}
}
"""
return Response(output=self.bridge.get_light())
def get_groups(self):
"""
Get the list of configured light groups.
:returns: List of configured light groups as id->dict.
Example::
output = {
"1": {
"name": "Living Room",
"lights": [
"16",
"13",
"12",
"11",
"10",
"9",
"1",
"3"
],
"type": "Room",
"state": {
"all_on": true,
"any_on": true
},
"class": "Living room",
"action": {
"on": true,
"bri": 241,
"hue": 37947,
"sat": 221,
"effect": "none",
"xy": [
0.2844,
0.2609
],
"ct": 153,
"alert": "none",
"colormode": "hs"
}
},
"2": {
# ...
}
}
"""
return Response(output=self.bridge.get_group())
@ -129,40 +259,132 @@ class LightHuePlugin(LightPlugin):
return Response(output='ok')
def set_light(self, light, **kwargs):
"""
Set a light (or lights) property.
:param light: Light or lights to set. Can be a string representing the light name, a light object, a list of string, or a list of light objects.
:param kwargs: key-value list of parameters to set.
Example call::
{
"type": "request",
"target": "hostname",
"action": "light.hue.set_light",
"args": {
"light": "Bulb 1",
"sat": 255
}
}
"""
self.connect()
self.bridge.set_light(light, **kwargs)
return Response(output='ok')
def set_group(self, group, **kwargs):
"""
Set a group (or groups) property.
:param group: Group or groups to set. Can be a string representing the group name, a group object, a list of strings, or a list of group objects.
:param kwargs: key-value list of parameters to set.
Example call::
{
"type": "request",
"target": "hostname",
"action": "light.hue.set_group",
"args": {
"light": "Living Room",
"sat": 255
}
}
"""
self.connect()
self.bridge.set_group(group, **kwargs)
return Response(output='ok')
def on(self, lights=[], groups=[]):
"""
Turn lights/groups on.
:param lights: Lights to turn on (names or light objects). Default: plugin default lights
:param groups: Groups to turn on (names or group objects). Default: plugin default groups
"""
return self._exec('on', True, lights=lights, groups=groups)
def off(self, lights=[], groups=[]):
"""
Turn lights/groups off.
:param lights: Lights to turn off (names or light objects). Default: plugin default lights
:param groups: Groups to turn off (names or group objects). Default: plugin default groups
"""
return self._exec('on', False, lights=lights, groups=groups)
def bri(self, value, lights=[], groups=[]):
"""
Set lights/groups brightness.
:param lights: Lights to control (names or light objects). Default: plugin default lights
:param groups: Groups to control (names or group objects). Default: plugin default groups
:param value: Brightness value (range: 0-255)
"""
return self._exec('bri', int(value) % (self.MAX_BRI+1),
lights=lights, groups=groups)
def sat(self, value, lights=[], groups=[]):
"""
Set lights/groups saturation.
:param lights: Lights to control (names or light objects). Default: plugin default lights
:param groups: Groups to control (names or group objects). Default: plugin default groups
:param value: Saturation value (range: 0-255)
"""
return self._exec('sat', int(value) % (self.MAX_SAT+1),
lights=lights, groups=groups)
def hue(self, value, lights=[], groups=[]):
"""
Set lights/groups color hue.
:param lights: Lights to control (names or light objects). Default: plugin default lights
:param groups: Groups to control (names or group objects). Default: plugin default groups
:param value: Hue value (range: 0-65535)
"""
return self._exec('hue', int(value) % (self.MAX_HUE+1),
lights=lights, groups=groups)
def scene(self, name, lights=[], groups=[]):
"""
Set a scene by name.
:param lights: Lights to control (names or light objects). Default: plugin default lights
:param groups: Groups to control (names or group objects). Default: plugin default groups
:param name: Name of the scene
"""
return self._exec('scene', name=name, lights=lights, groups=groups)
def is_animation_running(self):
"""
:returns: True if there is an animation running, false otherwise.
"""
return self.animation_thread is not None
def stop_animation(self):
"""
Stop a running animation if any
"""
if self.animation_thread and self.animation_thread.is_alive():
self.redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP')
@ -170,6 +392,40 @@ class LightHuePlugin(LightPlugin):
hue_range=[0, MAX_HUE], sat_range=[0, MAX_SAT],
bri_range=[MAX_BRI-1, MAX_BRI], lights=None, groups=None,
hue_step=1000, sat_step=2, bri_step=1, transition_seconds=1.0):
"""
Run a lights animation.
:param animation: Animation name. Supported types: **color_transition** and **blink**
:type animation: str
:param duration: Animation duration in seconds (default: None, i.e. continue until stop)
:type duration: float
:param hue_range: If you selected a color transition, this will specify the hue range of your color transition. Default: [0, 65535]
:type hue_range: list[int]
:param sat_range: If you selected a color transition, this will specify the saturation range of your color transition. Default: [0, 255]
:type sat_range: list[int]
:param bri_range: If you selected a color transition, this will specify the brightness range of your color transition. Default: [254, 255]
:type bri_range: list[int]
:param lights: Lights to control (names or light objects). Default: plugin default lights
:param groups: Groups to control (names or group objects). Default: plugin default groups
:param hue_step: If you selected a color transition, this will specify by how much the color hue will change between iterations. Default: 1000
:type hue_step: int
:param sat_step: If you selected a color transition, this will specify by how much the saturation will change between iterations. Default: 2
:type sat_step: int
:param bri_step: If you selected a color transition, this will specify by how much the brightness will change between iterations. Default: 1
:type bri_step: int
:param transition_seconds: Time between two transitions or blinks in seconds. Default: 1.0
:type treansition_seconds: float
"""
def _initialize_light_attrs(lights):
if animation == self.Animation.COLOR_TRANSITION:

View file

@ -7,14 +7,23 @@ from platypush.plugins import Plugin
class MidiPlugin(Plugin):
"""
Virtual MIDI controller plugin.
It requires python-rtmidi - https://pypi.org/project/python-rtmidi/
Virtual MIDI controller plugin. It allows you to send custom MIDI messages
to any connected devices.
Requires:
* **python-rtmidi** (``pip install python-rtmidi``)
"""
_played_notes = set()
def __init__(self, device_name='Platypush virtual MIDI output',
*args, **kwargs):
"""
:param device_name: MIDI virtual device name (default: *Platypush virtual MIDI output*)
:type device_name: str
"""
super().__init__(*args, **kwargs)
self.device_name = device_name
@ -32,30 +41,32 @@ class MidiPlugin(Plugin):
def send_message(self, values, *args, **kwargs):
"""
Values is expected to be a list containing the MIDI command code and
the command parameters - see reference at
https://ccrma.stanford.edu/~craig/articles/linuxmidi/misc/essenmidi.html
:param values: Values is expected to be a list containing the MIDI command code and the command parameters - see reference at https://ccrma.stanford.edu/~craig/articles/linuxmidi/misc/essenmidi.html
:type values: list[int]
Available MIDI commands:
0x80 Note Off
0x90 Note On
0xA0 Aftertouch
0xB0 Continuous controller
0xC0 Patch change
0xD0 Channel Pressure
0xE0 Pitch bend
0xF0 Start of system exclusive message
0xF1 MIDI Time Code Quarter Frame (Sys Common)
0xF2 Song Position Pointer (Sys Common)
0xF3 Song Select
0xF6 Tune Request (Sys Common)
0xF7 End of system exclusive message
0xF8 Timing Clock (Sys Realtime)
0xFA Start (Sys Realtime)
0xFB Continue (Sys Realtime)
0xFC Stop (Sys Realtime)
0xFE Active Sensing (Sys Realtime)
0xFF System Reset (Sys Realtime)
* ``0x80`` Note Off
* ``0x90`` Note On
* ``0xA0`` Aftertouch
* ``0xB0`` Continuous controller
* ``0xC0`` Patch change
* ``0xD0`` Channel Pressure
* ``0xE0`` Pitch bend
* ``0xF0`` Start of system exclusive message
* ``0xF1`` MIDI Time Code Quarter Frame (Sys Common)
* ``0xF2`` Song Position Pointer (Sys Common)
* ``0xF3`` Song Select
* ``0xF6`` Tune Request (Sys Common)
* ``0xF7`` End of system exclusive message
* ``0xF8`` Timing Clock (Sys Realtime)
* ``0xFA`` Start (Sys Realtime)
* ``0xFB`` Continue (Sys Realtime)
* ``0xFC`` Stop (Sys Realtime)
* ``0xFE`` Active Sensing (Sys Realtime)
* ``0xFF`` System Reset (Sys Realtime)
:param args: Extra args that will be passed to ``rtmidi.send_message``
:param kwargs: Extra kwargs that will be passed to ``rtmidi.send_message``
"""
self.midiout.send_message(values, *args, **kwargs)
@ -64,11 +75,16 @@ class MidiPlugin(Plugin):
def play_note(self, note, velocity, duration=0):
"""
Params:
- note: MIDI note in range 0-127 with #60 = C4
- velocity: MIDI note velocity in range 0-127
- duration: Note duration in (float) seconds. Pass 0 if you don't
want the note to get off
Play a note with selected velocity and duration.
:param note: MIDI note in range 0-127 with #60 = C4
:type note: int
:param velocity: MIDI note velocity in range 0-127
:type velocity: int
:param duration: Note duration in seconds. Pass 0 if you don't want the note to get off
:type duration: float
"""
self.send_message([0x90, note, velocity]) # Note on
@ -81,14 +97,26 @@ class MidiPlugin(Plugin):
def release_note(self, note):
"""
Release a played note.
:param note: MIDI note in range 0-127 with #60 = C4
:type note: int
"""
self.send_message([0x80, note, 0]) # Note off
self._played_notes.remove(note)
def release_all_notes(self):
"""
Release all the notes being played.
"""
played_notes = self._played_notes.copy()
for note in played_notes:
self.release_note(note)
# vim:sw=4:ts=4:et:

View file

@ -7,7 +7,27 @@ from platypush.plugins import Plugin
class MqttPlugin(Plugin):
"""
This plugin allows you to send custom message to a message queue compatible
with the MQTT protocol, see http://mqtt.org/
"""
def send_message(self, topic, msg, host, port=1883, *args, **kwargs):
"""
Sends a message to a topic/channel.
:param topic: Topic/channel where the message will be delivered
:type topic: str
:param msg: Message to be sent. It can be a list, a dict, or a Message object
:param host: MQTT broker hostname/IP
:type host: str
:param port: MQTT broker port (default: 1883)
:type port: int
"""
try: msg = json.dumps(msg)
except: pass

View file

@ -6,12 +6,27 @@ from platypush.message.response import Response
from .. import MusicPlugin
class MusicMpdPlugin(MusicPlugin):
def __init__(self, host, port):
"""
Constructor
Params:
host -- MPD host
port -- MPD port
This plugin allows you to interact with an MPD/Mopidy music server. MPD
(https://www.musicpd.org/) is a flexible server-side protocol/application
for handling music collections and playing music, mostly aimed to manage
local libraries. Mopidy (https://www.mopidy.com/) is an evolution of MPD,
compatible with the original protocol and with support for multiple music
sources through plugins (e.g. Spotify, TuneIn, Soundcloud, local files
etc.).
Requires:
* **python-mpd2** (``pip install python-mpd2``)
"""
def __init__(self, host, port=6600):
"""
:param host: MPD IP/hostname
:type host: str
:param port: MPD port (default: 6600)
:type port: int
"""
self.host = host
@ -24,50 +39,95 @@ class MusicMpdPlugin(MusicPlugin):
return self.status()
def play(self, resource=None):
"""
Play a resource by path/URI
:param resource: Resource path/URI
:type resource: str
"""
if resource:
self.clear()
self.add(resource)
return self._exec('play')
def play_pos(self, pos):
"""
Play a track in the current playlist by position number
:param pos: Position number
:type resource: int
"""
return self._exec('play', pos)
def pause(self):
""" Pause playback """
status = self.status().output['state']
if status == 'play': return self._exec('pause')
else: return self._exec('play')
def pause_if_playing(self):
""" Pause playback only if it's playing """
status = self.status().output['state']
if status == 'play': return self._exec('pause')
else: return Response(output={})
def play_if_paused(self):
""" Play only if it's paused (resume) """
status = self.status().output['state']
if status == 'pause': return self._exec('play')
else: return Response(output={})
def stop(self):
""" Stop playback """
return self._exec('stop')
def play_or_stop(self):
""" Play or stop (play state toggle) """
status = self.status().output['state']
if status == 'play': return self._exec('stop')
else: return self._exec('play')
def playid(self, track_id):
"""
Play a track by ID
:param track_id: Track ID
:type track_id: str
"""
return self._exec('playid', track_id)
def next(self):
""" Play the next track """
return self._exec('next')
def previous(self):
""" Play the previous track """
return self._exec('previous')
def setvol(self, vol):
"""
Set the volume
:param vol: Volume value (range: 0-100)
:type vol: int
"""
return self._exec('setvol', vol)
def volup(self, delta=10):
"""
Turn up the volume
:param delta: Volume up delta (default: +10%)
:type delta: int
"""
volume = int(self.status().output['volume'])
new_volume = volume+delta
if new_volume <= 100:
@ -75,6 +135,13 @@ class MusicMpdPlugin(MusicPlugin):
return self.status()
def voldown(self, delta=10):
"""
Turn down the volume
:param delta: Volume down delta (default: -10%)
:type delta: int
"""
volume = int(self.status().output['volume'])
new_volume = volume-delta
if new_volume >= 0:
@ -82,40 +149,125 @@ class MusicMpdPlugin(MusicPlugin):
return self.status()
def random(self, value=None):
"""
Set shuffle mode
:param value: If set, set the random/shuffle state this value (true/false). Default: None (toggle current state)
:type value: bool
"""
if value is None:
value = int(self.status().output['random'])
value = 1 if value == 0 else 0
return self._exec('random', value)
def repeat(self, value=None):
"""
Set repeat mode
:param value: If set, set the repeat state this value (true/false). Default: None (toggle current state)
:type value: bool
"""
if value is None:
value = int(self.status().output['repeat'])
value = 1 if value == 0 else 0
return self._exec('repeat', value)
def add(self, resource):
"""
Add a resource (track, album, artist, folder etc.) to the current playlist
:param resource: Resource path or URI
:type resource: str
"""
return self._exec('add', resource)
def load(self, playlist):
"""
Load and play a playlist by name
:param playlist: Playlist name
:type playlist: str
"""
self._exec('load', playlist)
return self.play()
def clear(self):
""" Clear the current playlist """
return self._exec('clear')
def seekcur(self, value):
"""
Seek to the specified position
:param value: Seek position in seconds, or delta string (e.g. '+15' or '-15') to indicate a seek relative to the current position
:type value: int
"""
return self._exec('seekcur', value)
def forward(self):
""" Go forward by 15 seconds """
return self._exec('seekcur', '+15')
def back(self):
""" Go backward by 15 seconds """
return self._exec('seekcur', '-15')
def status(self):
"""
:returns: The current state.
Example response::
output = {
"volume": "9",
"repeat": "0",
"random": "0",
"single": "0",
"consume": "0",
"playlist": "52",
"playlistlength": "14",
"xfade": "0",
"state": "play",
"song": "9",
"songid": "3061",
"nextsong": "10",
"nextsongid": "3062",
"time": "161:255",
"elapsed": "161.967",
"bitrate": "320"
}
"""
return Response(output=self.client.status())
def currentsong(self):
"""
:returns: The currently played track.
Example response::
output = {
"file": "spotify:track:7CO5ADlDN3DcR2pwlnB14P",
"time": "255",
"artist": "Elbow",
"album": "Little Fictions",
"title": "Kindling",
"date": "2017",
"track": "10",
"pos": "9",
"id": "3061",
"albumartist": "Elbow",
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
}
"""
track = self.client.currentsong()
if 'title' in track and ('artist' not in track
or not track['artist']
@ -128,20 +280,96 @@ class MusicMpdPlugin(MusicPlugin):
return Response(output=track)
def playlistinfo(self):
"""
:returns: The tracks in the current playlist as a list of dicts.
Example output::
output = [
{
"file": "spotify:track:79VtgIoznishPUDWO7Kafu",
"time": "355",
"artist": "Elbow",
"album": "Little Fictions",
"title": "Trust the Sun",
"date": "2017",
"track": "3",
"pos": "10",
"id": "3062",
"albumartist": "Elbow",
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
},
{
"file": "spotify:track:3EzTre0pxmoMYRuhJKMHj6",
"time": "219",
"artist": "Elbow",
"album": "Little Fictions",
"title": "Gentle Storm",
"date": "2017",
"track": "2",
"pos": "11",
"id": "3063",
"albumartist": "Elbow",
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
},
]
"""
return Response(output=self.client.playlistinfo())
def listplaylists(self):
"""
:returns: The playlists available on the server as a list of dicts.
Example response::
output = [
{
"playlist": "Rock",
"last-modified": "2018-06-25T21:28:19Z"
},
{
"playlist": "Jazz",
"last-modified": "2018-06-24T22:28:29Z"
},
{
# ...
}
]
"""
return Response(output=sorted(self.client.listplaylists(),
key=lambda p: p['playlist']))
def lsinfo(self, uri=None):
"""
Returns the list of playlists and directories on the server
"""
output = self.client.lsinfo(uri) if uri else self.client.lsinfo()
return Response(output=output)
def plchanges(self, version):
"""
Show what has changed on the current playlist since a specified playlist
version number.
:param version: Version number
:type version: int
:returns: A list of dicts representing the songs being added since the specified version
"""
return Response(output=self.client.plchanges(version))
def searchaddplaylist(self, name):
"""
Search and add a playlist by (partial or full) name
:param name: Playlist name, can be partial
:type name: str
"""
playlists = list(map(lambda _: _['playlist'],
filter(lambda playlist:
name.lower() in playlist['playlist'].lower(),
@ -156,14 +384,38 @@ class MusicMpdPlugin(MusicPlugin):
return Response(output={})
def find(self, filter, *args, **kwargs):
"""
Find in the database/library by filter.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
return Response(
output=self.client.find(*filter, *args, **kwargs))
def findadd(self, filter, *args, **kwargs):
"""
Find in the database/library by filter and add to the current playlist.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
return Response(
output=self.client.findadd(*filter, *args, **kwargs))
def search(self, filter, *args, **kwargs):
"""
Free search by filter.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
items = self.client.search(*filter, *args, **kwargs)
# Spotify results first
@ -173,6 +425,14 @@ class MusicMpdPlugin(MusicPlugin):
return Response(output=items)
def searchadd(self, filter, *args, **kwargs):
"""
Free search by filter and add the results to the current playlist.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
return Response(
output=self.client.searchadd(*filter, *args, **kwargs))