diff --git a/platypush/backend/nodered/__init__.py b/platypush/backend/nodered/__init__.py new file mode 100644 index 000000000..a193dae60 --- /dev/null +++ b/platypush/backend/nodered/__init__.py @@ -0,0 +1,47 @@ +import inspect +import os +import subprocess +import sys + +from platypush.backend import Backend + + +class NoderedBackend(Backend): + """ + This backend publishes platypush actions to a Node-RED instance. + If you enable this backend on a host that runs Node-RED then a new block (platypush -> run) can be + used in your flows. This block will accept JSON requests as input in the format + ``{"type":"request", "action":"plugin.name.action_name", "args": {...}}`` and return the output + of the action as block output, or raise an exception if the action failed. + + Requires: + + * **pynodered** (``pip install pynodered``) + """ + + def __init__(self, port: int = 5051, *args, **kwargs): + """ + :param port: Listening port for the local publishing web server (default: 5051) + """ + super().__init__(*args, **kwargs) + self.port = port + self._runner_path = os.path.join( + os.path.dirname(inspect.getfile(self.__class__)), 'runner.py') + self._server = None + + def on_stop(self): + if self._server: + self._server.terminate() + self._server = None + + def run(self): + super().run() + + self._server = subprocess.Popen([sys.executable, '-m', 'pynodered.server', + '--port', str(self.port), self._runner_path]) + + self.logger.info('Started Node-RED backend on port {}'.format(self.port)) + self._server.wait() + + +# vim:sw=4:ts=4:et: diff --git a/platypush/backend/nodered/runner.py b/platypush/backend/nodered/runner.py new file mode 100644 index 000000000..d8ef1ee0b --- /dev/null +++ b/platypush/backend/nodered/runner.py @@ -0,0 +1,35 @@ +import json + +from pynodered import node_red +from platypush.context import get_plugin + + +# noinspection PyUnusedLocal +@node_red(name='run', title='run', category='platypush', description='Run a platypush action') +def run(node, msg): + msg = msg['payload'] + if isinstance(msg, bytes): + msg = msg.decode() + if isinstance(msg, str): + msg = json.loads(msg) + + assert isinstance(msg, dict) and 'action' in msg + + if 'type' not in msg: + msg['type'] = 'request' + + plugin_name = '.'.join(msg['action'].split('.')[:-1]) + action_name = msg['action'].split('.')[-1] + plugin = get_plugin(plugin_name) + action = getattr(plugin, action_name) + args = msg.get('args', {}) + + response = action(**args) + if response.errors: + raise response.errors[0] + + msg['payload'] = response.output + return msg + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index ce4b80c7b..62e8d0755 100644 --- a/requirements.txt +++ b/requirements.txt @@ -184,3 +184,6 @@ croniter # Support for RST->HTML docstring conversion # docutils + +# Support for Node-RED integration +# nodered diff --git a/setup.py b/setup.py index 292e07435..87570cba9 100755 --- a/setup.py +++ b/setup.py @@ -248,6 +248,8 @@ setup( 'cv': ['cv2', 'numpy'], # Support for the generation of HTML documentation from docstring 'htmldoc': ['docutils'], + # Support for Node-RED integration + 'nodered': ['pynodered'], }, )