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.mail.rst
platypush/plugins/google.maps.rst platypush/plugins/google.maps.rst
platypush/plugins/gpio.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): 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_measurement(self, *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') raise NotImplementedError('get_measurement should be implemented in a derived class')
def get_data(self, *args, **kwargs): def get_data(self, *args, **kwargs):
"""
Alias for ``get_measurement``
"""
return self.get_measurement(*args, **kwargs) return self.get_measurement(*args, **kwargs)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -1,14 +1,30 @@
import threading import threading
import time import time
import RPi.GPIO as gpio
from platypush.message.response import Response from platypush.message.response import Response
from platypush.plugins.gpio.sensor import GpioSensorPlugin from platypush.plugins.gpio.sensor import GpioSensorPlugin
class GpioSensorDistancePlugin(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): 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) super().__init__(*args, **kwargs)
self.trigger_pin = trigger_pin self.trigger_pin = trigger_pin
@ -24,6 +40,13 @@ class GpioSensorDistancePlugin(GpioSensorPlugin):
def get_measurement(self): 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.setmode(gpio.BCM)
gpio.setup(self.trigger_pin, gpio.OUT) gpio.setup(self.trigger_pin, gpio.OUT)
gpio.setup(self.echo_pin, gpio.IN) gpio.setup(self.echo_pin, gpio.IN)

View File

@ -1,9 +1,6 @@
import enum import enum
import time import time
import Adafruit_GPIO.SPI as SPI
import Adafruit_MCP3008
from platypush.plugins.gpio.sensor import GpioSensorPlugin from platypush.plugins.gpio.sensor import GpioSensorPlugin
from platypush.message.response import Response from platypush.message.response import Response
@ -15,15 +12,78 @@ class MCP3008Mode(enum.Enum):
class GpioSensorMcp3008Plugin(GpioSensorPlugin): class GpioSensorMcp3008Plugin(GpioSensorPlugin):
""" """
Plugin to read analog sensor values from an MCP3008 chipset, Plugin to read analog sensor values from an MCP3008 chipset. The MCP3008
see https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008 chipset is a circuit that allows you to read measuremnts from multiple
Requires adafruit-mcp3008 Python package 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 N_CHANNELS = 8
def __init__(self, CLK=None, MISO=None, MOSI=None, CS=None, spi_port=None, def __init__(self, CLK=None, MISO=None, MOSI=None, CS=None, spi_port=None,
spi_device=None, channels=None, Vdd=3.3, *args, **kwargs): 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) super().__init__(*args, **kwargs)
if CLK and MISO and MOSI and CS: if CLK and MISO and MOSI and CS:
@ -47,6 +107,9 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
def _get_mcp(self): def _get_mcp(self):
import Adafruit_GPIO.SPI as SPI
import Adafruit_MCP3008
if self.mode == MCP3008Mode.SOFTWARE: if self.mode == MCP3008Mode.SOFTWARE:
self.mcp = Adafruit_MCP3008.MCP3008(clk=self.CLK, cs=self.CS, self.mcp = Adafruit_MCP3008.MCP3008(clk=self.CLK, cs=self.CS,
miso=self.MISO, mosi=self.MOSI) miso=self.MISO, mosi=self.MOSI)
@ -63,6 +126,27 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
def get_measurement(self): 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() mcp = self._get_mcp()
values = {} values = {}

View File

@ -18,12 +18,65 @@ class Direction(enum.Enum):
class GpioZeroborgPlugin(Plugin): 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 _drive_thread = None
_can_run = False _can_run = False
_direction = None _direction = None
def __init__(self, directions = {}, *args, **kwargs): 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 import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -75,6 +128,16 @@ class GpioZeroborgPlugin(Plugin):
def drive(self, direction): 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 prev_direction = self._direction
self._can_run = True self._can_run = True
@ -125,6 +188,10 @@ class GpioZeroborgPlugin(Plugin):
def stop(self): def stop(self):
"""
Turns off the motors
"""
self._can_run = False self._can_run = False
if self._drive_thread and threading.get_ident() != self._drive_thread.ident: if self._drive_thread and threading.get_ident() != self._drive_thread.ident:
self._drive_thread.join() self._drive_thread.join()

View File

@ -5,7 +5,41 @@ from platypush.message.response import Response
from platypush.plugins import Plugin from platypush.plugins import Plugin
class HttpRequestPlugin(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): def _exec(self, method, url, output='text', **kwargs):
""" Available output types: text (default), json, binary """ """ Available output types: text (default), json, binary """
@ -21,26 +55,86 @@ class HttpRequestPlugin(Plugin):
def get(self, url, **kwargs): 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) return self._exec(method='get', url=url, **kwargs)
def post(self, 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) return self._exec(method='post', url=url, **kwargs)
def head(self, 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) return self._exec(method='head', url=url, **kwargs)
def put(self, 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) return self._exec(method='put', url=url, **kwargs)
def delete(self, 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) return self._exec(method='delete', url=url, **kwargs)
def options(self, 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) return self._exec(method='options', url=url, **kwargs)

View File

@ -6,7 +6,30 @@ from platypush.message.response import Response
from .. import Plugin from .. import Plugin
class LastfmPlugin(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): 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_key = api_key
self.api_secret = api_secret self.api_secret = api_secret
self.username = username self.username = username
@ -20,6 +43,17 @@ class LastfmPlugin(Plugin):
def scrobble(self, artist, title, album=None, **kwargs): 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( self.lastfm.scrobble(
artist = artist, artist = artist,
title = title, title = title,
@ -31,6 +65,17 @@ class LastfmPlugin(Plugin):
def update_now_playing(self, artist, title, album=None, **kwargs): 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( self.lastfm.update_now_playing(
artist = artist, artist = artist,
title = title, title = title,

View File

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

View File

@ -12,7 +12,13 @@ from platypush.message.response import Response
from .. import LightPlugin from .. import LightPlugin
class LightHuePlugin(LightPlugin): class LightHuePlugin(LightPlugin):
""" Philips Hue lights plugin """ """
Philips Hue lights plugin.
Requires:
* **phue** (``pip install phue``)
"""
MAX_BRI = 255 MAX_BRI = 255
MAX_SAT = 255 MAX_SAT = 255
@ -31,11 +37,14 @@ class LightHuePlugin(LightPlugin):
def __init__(self, bridge, lights=None, groups=None): def __init__(self, bridge, lights=None, groups=None):
""" """
Constructor :param bridge: Bridge address or hostname
Params: :type bridge: str
bridge -- Bridge address or hostname
lights -- Lights to be controlled (default: all) :param lights: Default lights to be controlled (default: all)
groups -- Groups to be controlled (default: all) :type lights: list[str]
:param groups Default groups to be controlled (default: all)
:type groups: list[str]
""" """
super().__init__() super().__init__()
@ -66,6 +75,14 @@ class LightHuePlugin(LightPlugin):
self.lights.extend([l.name for l in g.lights]) self.lights.extend([l.name for l in g.lights])
def connect(self): 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 # Lazy init
if not self.bridge: if not self.bridge:
self.bridge = Bridge(self.bridge_address) self.bridge = Bridge(self.bridge_address)
@ -83,14 +100,127 @@ class LightHuePlugin(LightPlugin):
def get_scenes(self): 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()) return Response(output=self.bridge.get_scene())
def get_lights(self): 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()) return Response(output=self.bridge.get_light())
def get_groups(self): 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()) return Response(output=self.bridge.get_group())
@ -129,40 +259,132 @@ class LightHuePlugin(LightPlugin):
return Response(output='ok') return Response(output='ok')
def set_light(self, light, **kwargs): 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.connect()
self.bridge.set_light(light, **kwargs) self.bridge.set_light(light, **kwargs)
return Response(output='ok') return Response(output='ok')
def set_group(self, group, **kwargs): 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.connect()
self.bridge.set_group(group, **kwargs) self.bridge.set_group(group, **kwargs)
return Response(output='ok') return Response(output='ok')
def on(self, lights=[], groups=[]): 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) return self._exec('on', True, lights=lights, groups=groups)
def off(self, lights=[], 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) return self._exec('on', False, lights=lights, groups=groups)
def bri(self, value, lights=[], 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), return self._exec('bri', int(value) % (self.MAX_BRI+1),
lights=lights, groups=groups) lights=lights, groups=groups)
def sat(self, value, lights=[], 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), return self._exec('sat', int(value) % (self.MAX_SAT+1),
lights=lights, groups=groups) lights=lights, groups=groups)
def hue(self, value, lights=[], 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), return self._exec('hue', int(value) % (self.MAX_HUE+1),
lights=lights, groups=groups) lights=lights, groups=groups)
def scene(self, name, lights=[], 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) return self._exec('scene', name=name, lights=lights, groups=groups)
def is_animation_running(self): def is_animation_running(self):
"""
:returns: True if there is an animation running, false otherwise.
"""
return self.animation_thread is not None return self.animation_thread is not None
def stop_animation(self): def stop_animation(self):
"""
Stop a running animation if any
"""
if self.animation_thread and self.animation_thread.is_alive(): if self.animation_thread and self.animation_thread.is_alive():
self.redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP') 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], hue_range=[0, MAX_HUE], sat_range=[0, MAX_SAT],
bri_range=[MAX_BRI-1, MAX_BRI], lights=None, groups=None, bri_range=[MAX_BRI-1, MAX_BRI], lights=None, groups=None,
hue_step=1000, sat_step=2, bri_step=1, transition_seconds=1.0): 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): def _initialize_light_attrs(lights):
if animation == self.Animation.COLOR_TRANSITION: if animation == self.Animation.COLOR_TRANSITION:

View File

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

View File

@ -7,7 +7,27 @@ from platypush.plugins import Plugin
class MqttPlugin(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): 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) try: msg = json.dumps(msg)
except: pass except: pass

View File

@ -6,12 +6,27 @@ from platypush.message.response import Response
from .. import MusicPlugin from .. import MusicPlugin
class MusicMpdPlugin(MusicPlugin): class MusicMpdPlugin(MusicPlugin):
def __init__(self, host, 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):
""" """
Constructor :param host: MPD IP/hostname
Params: :type host: str
host -- MPD host
port -- MPD port :param port: MPD port (default: 6600)
:type port: int
""" """
self.host = host self.host = host
@ -24,50 +39,95 @@ class MusicMpdPlugin(MusicPlugin):
return self.status() return self.status()
def play(self, resource=None): def play(self, resource=None):
"""
Play a resource by path/URI
:param resource: Resource path/URI
:type resource: str
"""
if resource: if resource:
self.clear() self.clear()
self.add(resource) self.add(resource)
return self._exec('play') return self._exec('play')
def play_pos(self, pos): 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) return self._exec('play', pos)
def pause(self): def pause(self):
""" Pause playback """
status = self.status().output['state'] status = self.status().output['state']
if status == 'play': return self._exec('pause') if status == 'play': return self._exec('pause')
else: return self._exec('play') else: return self._exec('play')
def pause_if_playing(self): def pause_if_playing(self):
""" Pause playback only if it's playing """
status = self.status().output['state'] status = self.status().output['state']
if status == 'play': return self._exec('pause') if status == 'play': return self._exec('pause')
else: return Response(output={}) else: return Response(output={})
def play_if_paused(self): def play_if_paused(self):
""" Play only if it's paused (resume) """
status = self.status().output['state'] status = self.status().output['state']
if status == 'pause': return self._exec('play') if status == 'pause': return self._exec('play')
else: return Response(output={}) else: return Response(output={})
def stop(self): def stop(self):
""" Stop playback """
return self._exec('stop') return self._exec('stop')
def play_or_stop(self): def play_or_stop(self):
""" Play or stop (play state toggle) """
status = self.status().output['state'] status = self.status().output['state']
if status == 'play': return self._exec('stop') if status == 'play': return self._exec('stop')
else: return self._exec('play') else: return self._exec('play')
def playid(self, track_id): 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) return self._exec('playid', track_id)
def next(self): def next(self):
""" Play the next track """
return self._exec('next') return self._exec('next')
def previous(self): def previous(self):
""" Play the previous track """
return self._exec('previous') return self._exec('previous')
def setvol(self, vol): def setvol(self, vol):
"""
Set the volume
:param vol: Volume value (range: 0-100)
:type vol: int
"""
return self._exec('setvol', vol) return self._exec('setvol', vol)
def volup(self, delta=10): 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']) volume = int(self.status().output['volume'])
new_volume = volume+delta new_volume = volume+delta
if new_volume <= 100: if new_volume <= 100:
@ -75,6 +135,13 @@ class MusicMpdPlugin(MusicPlugin):
return self.status() return self.status()
def voldown(self, delta=10): 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']) volume = int(self.status().output['volume'])
new_volume = volume-delta new_volume = volume-delta
if new_volume >= 0: if new_volume >= 0:
@ -82,40 +149,125 @@ class MusicMpdPlugin(MusicPlugin):
return self.status() return self.status()
def random(self, value=None): 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: if value is None:
value = int(self.status().output['random']) value = int(self.status().output['random'])
value = 1 if value == 0 else 0 value = 1 if value == 0 else 0
return self._exec('random', value) return self._exec('random', value)
def repeat(self, value=None): 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: if value is None:
value = int(self.status().output['repeat']) value = int(self.status().output['repeat'])
value = 1 if value == 0 else 0 value = 1 if value == 0 else 0
return self._exec('repeat', value) return self._exec('repeat', value)
def add(self, resource): 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) return self._exec('add', resource)
def load(self, playlist): def load(self, playlist):
"""
Load and play a playlist by name
:param playlist: Playlist name
:type playlist: str
"""
self._exec('load', playlist) self._exec('load', playlist)
return self.play() return self.play()
def clear(self): def clear(self):
""" Clear the current playlist """
return self._exec('clear') return self._exec('clear')
def seekcur(self, value): 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) return self._exec('seekcur', value)
def forward(self): def forward(self):
""" Go forward by 15 seconds """
return self._exec('seekcur', '+15') return self._exec('seekcur', '+15')
def back(self): def back(self):
""" Go backward by 15 seconds """
return self._exec('seekcur', '-15') return self._exec('seekcur', '-15')
def status(self): 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()) return Response(output=self.client.status())
def currentsong(self): 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() track = self.client.currentsong()
if 'title' in track and ('artist' not in track if 'title' in track and ('artist' not in track
or not track['artist'] or not track['artist']
@ -128,20 +280,96 @@ class MusicMpdPlugin(MusicPlugin):
return Response(output=track) return Response(output=track)
def playlistinfo(self): 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()) return Response(output=self.client.playlistinfo())
def listplaylists(self): 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(), return Response(output=sorted(self.client.listplaylists(),
key=lambda p: p['playlist'])) key=lambda p: p['playlist']))
def lsinfo(self, uri=None): 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() output = self.client.lsinfo(uri) if uri else self.client.lsinfo()
return Response(output=output) return Response(output=output)
def plchanges(self, version): 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)) return Response(output=self.client.plchanges(version))
def searchaddplaylist(self, name): 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'], playlists = list(map(lambda _: _['playlist'],
filter(lambda playlist: filter(lambda playlist:
name.lower() in playlist['playlist'].lower(), name.lower() in playlist['playlist'].lower(),
@ -156,14 +384,38 @@ class MusicMpdPlugin(MusicPlugin):
return Response(output={}) return Response(output={})
def find(self, filter, *args, **kwargs): 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( return Response(
output=self.client.find(*filter, *args, **kwargs)) output=self.client.find(*filter, *args, **kwargs))
def findadd(self, 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( return Response(
output=self.client.findadd(*filter, *args, **kwargs)) output=self.client.findadd(*filter, *args, **kwargs))
def search(self, 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) items = self.client.search(*filter, *args, **kwargs)
# Spotify results first # Spotify results first
@ -173,6 +425,14 @@ class MusicMpdPlugin(MusicPlugin):
return Response(output=items) return Response(output=items)
def searchadd(self, filter, *args, **kwargs): 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( return Response(
output=self.client.searchadd(*filter, *args, **kwargs)) output=self.client.searchadd(*filter, *args, **kwargs))