node-redvolttron

TypeError: PythonShell is not a constructor


I am experimenting with a node red - VOLTTRON (Python framework) integration where I am hoping to view the VOLTTRON message bus in Node Red.

When I do the appropriate steps as defined in the README like copying the files over to the correct ~/.node-red/nodes/volttron and getting the correct VOLTTRON authentication keys

When I start Node Red, I get an error:

12 Dec 14:07:54 - [info] Server now running at http://127.0.0.1:8000/
12 Dec 14:07:54 - [info] Starting flows
12 Dec 14:07:54 - [info] Started flows
12 Dec 14:07:54 - [warn] [volttron-input:03074cce8abc3174] /home/ben/.node-red/nodes/volttron
12 Dec 14:07:54 - [warn] [volttron-input:03074cce8abc3174] /home/ben/.node-red/nodes/volttron/node_red_subscriber.py
12 Dec 14:07:54 - [red] Uncaught Exception:
12 Dec 14:07:54 - TypeError: PythonShell is not a constructor
    at Timeout._onTimeout (/home/ben/.node-red/nodes/volttron/volttron.js:33:23)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)

I am unsure if this would be a problem associated with javascript or Python? Thanks for any tips next to zero wisdom here.

volttron.js

module.exports = function(RED) {
    // Set these variables to be valid file paths
    var volttron_env = '/home/ben/.volttron';
    var volttron_home = '/home/ben/Desktop/volttron';
    var python_path = '/usr/lib/python3.8';

    function VolttronInputNode(config) {
        RED.nodes.createNode(this,config);
        var node = this;
        var pyshell = null;
        this.on('close', function(done) {
            setTimeout(function() {
                /* do some asynchronous cleanup before calling done */
                if (pyshell && !pyshell.terminated && pyshell.childProcess)
                    pyshell.childProcess.kill('SIGINT');
                done();
            });
        });
        setTimeout(function() {
            var PythonShell = require('python-shell');
            process.env['VIRTUAL_ENV'] = volttron_env;
            process.env['VOLTTRON_HOME'] = volttron_home;
            var options = {
                mode: 'json',
                pythonPath: python_path,
                scriptPath: __dirname,
            };
            var path = require('path');
            node.warn(__dirname);
            var scriptName = 'node_red_subscriber.py';
            var scriptPath = path.resolve(__dirname, scriptName);
            node.warn(scriptPath);
            pyshell = new PythonShell(scriptName, options);

            pyshell.on('message', function (data) {
                msg = {};
                msg.topic = node.name;
                msg.payload = data;
                node.send(msg);
            });

            pyshell.end(function (err) {
                node.error(err);
            });

        });
    }
    RED.nodes.registerType("volttron-input", VolttronInputNode);

    function VolttronOutputNode(config) {
        RED.nodes.createNode(this,config);
        var node = this;
        this.on('close', function(done) {
            setTimeout(function() { /* do some asynchronous cleanup before calling done */ done(); });
        });
        this.on("input", function (msg) {
            setTimeout(function() { // send asynchronously
                var PythonShell = require('python-shell');
                process.env['VIRTUAL_ENV'] = volttron_env;
                process.env['VOLTTRON_HOME'] = volttron_home;
                var options = {
                    mode: 'json',
                    pythonPath: python_path,
                    scriptPath: __dirname,
                    args: [msg.payload.topic, msg.payload.data]
                };
                var path = require('path');
                var scriptName = 'node_red_publisher.py';
                var scriptPath = path.resolve(__dirname, scriptName);
                PythonShell.run(scriptName, options, function(err, result) {
                    if (err) node.error(err);
                    if (result) node.warn(result);
                });
            });

        });
    }
    RED.nodes.registerType("volttron-output",VolttronOutputNode);

}

node_red_subscriber.py

from datetime import datetime
import os
import sys


import gevent

from volttron.platform.messaging import headers as headers_mod
from volttron.platform.vip.agent import Agent, PubSub, Core
from volttron.platform.agent import utils
from volttron.platform.scheduling import periodic
from volttron.platform import jsonapi

from settings import topic_prefixes_to_watch, heartbeat_period, agent_kwargs


class NodeRedSubscriber(Agent):

    def onmessage(self, peer, sender, bus, topic, headers, message):
        d = {'topic': topic,
             'headers': headers,
             'message': message}
        sys.stdout.write(jsonapi.dumps(d)+'\n')
        sys.stdout.flush()

    @Core.receiver('onstart')
    def onstart(self, sender, **kwargs):
        for prefix in topic_prefixes_to_watch:
            self.vip.pubsub.subscribe(peer='pubsub', prefix=prefix, callback=self.onmessage).get(timeout=10)

    # Demonstrate periodic decorator and settings access
    @Core.schedule(periodic(heartbeat_period))
    def publish_heartbeat(self):
        now = utils.format_timestamp(datetime.utcnow())
        headers = {
            headers_mod.CONTENT_TYPE: headers_mod.CONTENT_TYPE.PLAIN_TEXT,
            headers_mod.DATE: now,
            headers_mod.TIMESTAMP: now
        }
        result = self.vip.pubsub.publish('pubsub', 'heartbeat/NodeRedSubscriber', headers, now)
        result.get(timeout=10)


if  __name__ == '__main__':
    try:
        # If stdout is a pipe, re-open it line buffered
        if utils.isapipe(sys.stdout):
            # Hold a reference to the previous file object so it doesn't
            # get garbage collected and close the underlying descriptor.
            stdout = sys.stdout
            sys.stdout = os.fdopen(stdout.fileno(), 'w', 1)

        agent = NodeRedSubscriber(identity='NodeRedSubscriber', **agent_kwargs)
        task = gevent.spawn(agent.core.run)

        try:
            task.join()
        finally:
            task.kill()

    except KeyboardInterrupt:
        pass

EDIT Trying some steps further, in the directory .node-red/node_modules I placed one file called package.json with the contents provided in the answer with the dependency "python-shell": "^1.0.4" as provided in the answer. And then from this same directory running from bash some npm errors come up:

~/.node-red/node_modules$ npm install volttron

traceback:

npm ERR! code E404
npm ERR! 404 Not Found - GET https://registry.npmjs.org/volttron - Not found
npm ERR! 404
npm ERR! 404  'volttron@*' is not in this registry.
npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
npm ERR! 404
npm ERR! 404 Note that you can also install from a
npm ERR! 404 tarball, folder, http url, or git url.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/ben/.npm/_logs/2021-12-13T12_52_57_550Z-debug.log

In the other directory ~/.node-red/nodes/volttron

These files are present:

node_red_publisher.py  node_red_subscriber.py  README  settings.py  volttron.html  volttron.js

Solution

  • In the previous answer I had to guess which version of python-shell was used by the node as it has no hints, so I picked the current latest version (3.0.1) as an arbitrary choice.

    It appears that this was the wrong choice soI suggest you edit the package.json file again and change the ^3.0.1 version for the python-shell dependency and change it to ^1.0.4

    {
        "name"         : "volttron",
        "version"      : "0.0.1",
        "description"  : "A sample node for node-red",
        "dependencies": {
            "python-shell": "^1.0.4"
        },
        "keywords": [ "node-red" ],
        "node-red"     : {
            "nodes": {
                "volttron": "volttron.js"
            }
        }
    }
    

    You will need to run npm install again in the volttron directory after making the change.

    If this is not the case then you will have to take this up with Volttron.