diff --git a/docs/source/platypush/plugins/gpio.sensor.distance.rst b/docs/source/platypush/plugins/gpio.sensor.distance.rst new file mode 100644 index 00000000..0d7ce18c --- /dev/null +++ b/docs/source/platypush/plugins/gpio.sensor.distance.rst @@ -0,0 +1,6 @@ +``platypush.plugins.gpio.sensor.distance`` +========================================== + +.. automodule:: platypush.plugins.gpio.sensor.distance + :members: + diff --git a/docs/source/platypush/plugins/gpio.sensor.mcp3008.rst b/docs/source/platypush/plugins/gpio.sensor.mcp3008.rst new file mode 100644 index 00000000..59e68f82 --- /dev/null +++ b/docs/source/platypush/plugins/gpio.sensor.mcp3008.rst @@ -0,0 +1,7 @@ +``platypush.plugins.gpio.sensor.mcp3008`` +========================================= + +.. automodule:: platypush.plugins.gpio.sensor.mcp3008 + :members: + + diff --git a/docs/source/platypush/plugins/gpio.sensor.rst b/docs/source/platypush/plugins/gpio.sensor.rst new file mode 100644 index 00000000..16cf8a9a --- /dev/null +++ b/docs/source/platypush/plugins/gpio.sensor.rst @@ -0,0 +1,6 @@ +``platypush.plugins.gpio.sensor`` +================================= + +.. automodule:: platypush.plugins.gpio.sensor + :members: + diff --git a/docs/source/platypush/plugins/gpio.zeroborg.rst b/docs/source/platypush/plugins/gpio.zeroborg.rst new file mode 100644 index 00000000..2b4890a1 --- /dev/null +++ b/docs/source/platypush/plugins/gpio.zeroborg.rst @@ -0,0 +1,6 @@ +``platypush.plugins.gpio.zeroborg`` +=================================== + +.. automodule:: platypush.plugins.gpio.zeroborg + :members: + diff --git a/docs/source/platypush/plugins/http.request.rst b/docs/source/platypush/plugins/http.request.rst new file mode 100644 index 00000000..fc35689d --- /dev/null +++ b/docs/source/platypush/plugins/http.request.rst @@ -0,0 +1,7 @@ +``platypush.plugins.http.request`` +================================== + +.. automodule:: platypush.plugins.http.request + :members: + + diff --git a/docs/source/platypush/plugins/light.hue.rst b/docs/source/platypush/plugins/light.hue.rst new file mode 100644 index 00000000..61b211de --- /dev/null +++ b/docs/source/platypush/plugins/light.hue.rst @@ -0,0 +1,6 @@ +``platypush.plugins.light.hue`` +=============================== + +.. automodule:: platypush.plugins.light.hue + :members: + diff --git a/docs/source/platypush/plugins/light.rst b/docs/source/platypush/plugins/light.rst new file mode 100644 index 00000000..4c2b5a64 --- /dev/null +++ b/docs/source/platypush/plugins/light.rst @@ -0,0 +1,6 @@ +``platypush.plugins.light`` +=========================== + +.. automodule:: platypush.plugins.light + :members: + diff --git a/docs/source/platypush/plugins/midi.rst b/docs/source/platypush/plugins/midi.rst new file mode 100644 index 00000000..b62d23f8 --- /dev/null +++ b/docs/source/platypush/plugins/midi.rst @@ -0,0 +1,6 @@ +``platypush.plugins.midi`` +========================== + +.. automodule:: platypush.plugins.midi + :members: + diff --git a/docs/source/platypush/plugins/mqtt.rst b/docs/source/platypush/plugins/mqtt.rst new file mode 100644 index 00000000..4fe55a00 --- /dev/null +++ b/docs/source/platypush/plugins/mqtt.rst @@ -0,0 +1,7 @@ +``platypush.plugins.mqtt`` +========================== + +.. automodule:: platypush.plugins.mqtt + :members: + + diff --git a/docs/source/platypush/plugins/music.mpd.rst b/docs/source/platypush/plugins/music.mpd.rst new file mode 100644 index 00000000..4ebc9b70 --- /dev/null +++ b/docs/source/platypush/plugins/music.mpd.rst @@ -0,0 +1,6 @@ +``platypush.plugins.music.mpd`` +=============================== + +.. automodule:: platypush.plugins.music.mpd + :members: + diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 6946b7f3..4a6073ed 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -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 diff --git a/platypush/plugins/gpio/sensor/__init__.py b/platypush/plugins/gpio/sensor/__init__.py index a25a2c35..29e0af05 100644 --- a/platypush/plugins/gpio/sensor/__init__.py +++ b/platypush/plugins/gpio/sensor/__init__.py @@ -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: diff --git a/platypush/plugins/gpio/sensor/distance/__init__.py b/platypush/plugins/gpio/sensor/distance/__init__.py index 18441c9f..97e6162f 100644 --- a/platypush/plugins/gpio/sensor/distance/__init__.py +++ b/platypush/plugins/gpio/sensor/distance/__init__.py @@ -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) diff --git a/platypush/plugins/gpio/sensor/mcp3008/__init__.py b/platypush/plugins/gpio/sensor/mcp3008/__init__.py index 7c37134b..520b7139 100644 --- a/platypush/plugins/gpio/sensor/mcp3008/__init__.py +++ b/platypush/plugins/gpio/sensor/mcp3008/__init__.py @@ -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 = {} diff --git a/platypush/plugins/gpio/zeroborg/__init__.py b/platypush/plugins/gpio/zeroborg/__init__.py index b1e25031..e725d4bb 100644 --- a/platypush/plugins/gpio/zeroborg/__init__.py +++ b/platypush/plugins/gpio/zeroborg/__init__.py @@ -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() diff --git a/platypush/plugins/http/request/__init__.py b/platypush/plugins/http/request/__init__.py index b0ed179d..60f3cb87 100644 --- a/platypush/plugins/http/request/__init__.py +++ b/platypush/plugins/http/request/__init__.py @@ -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) diff --git a/platypush/plugins/lastfm/__init__.py b/platypush/plugins/lastfm/__init__.py index 434874f8..747cf391 100644 --- a/platypush/plugins/lastfm/__init__.py +++ b/platypush/plugins/lastfm/__init__.py @@ -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, diff --git a/platypush/plugins/light/__init__.py b/platypush/plugins/light/__init__.py index 0dd02f42..f4fa7a34 100644 --- a/platypush/plugins/light/__init__.py +++ b/platypush/plugins/light/__init__.py @@ -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() diff --git a/platypush/plugins/light/hue/__init__.py b/platypush/plugins/light/hue/__init__.py index 0368ba14..7d111d50 100644 --- a/platypush/plugins/light/hue/__init__.py +++ b/platypush/plugins/light/hue/__init__.py @@ -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: diff --git a/platypush/plugins/midi.py b/platypush/plugins/midi.py index 2c954255..501fb3d9 100644 --- a/platypush/plugins/midi.py +++ b/platypush/plugins/midi.py @@ -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: diff --git a/platypush/plugins/mqtt.py b/platypush/plugins/mqtt.py index b1e3c22b..7a6615b2 100644 --- a/platypush/plugins/mqtt.py +++ b/platypush/plugins/mqtt.py @@ -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 diff --git a/platypush/plugins/music/mpd/__init__.py b/platypush/plugins/music/mpd/__init__.py index 56b98c02..92141364 100644 --- a/platypush/plugins/music/mpd/__init__.py +++ b/platypush/plugins/music/mpd/__init__.py @@ -6,12 +6,27 @@ from platypush.message.response import Response from .. import 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 - Params: - host -- MPD host - port -- MPD port + :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))