Skip to main content

Testing a new REST API in Node-RED

Koh Rong, Cambodia

In the previous step I created a mocked API for an INSTAR IP camera. Now that I have a new basic structure in place I want to connect this camera to Node-RED and translate the old API into my new one. I am going to use the INSTAR MQTT API to provide the camera state as well as to send state updates back to my camera. This will simulate the API being active directly on my camera and allows me to interact with my camera using the new HTTP REST API calls. So much the theory... let's make this work.

MQTT Intermediate

The INSTAR MQTT API is documented in detail on the INSTAR Wiki:

For testing I want to start manipulating the Privacy Areas of my camera - as this is something that will have an immediate effect so I will be able to see if it is working or not. The camera has 4 privacy areas that can be used to mask a part of the image. Each area in turn is controlled by 6 MQTT Topics:

MQTT TopicDescription
prefix/cameraid/status/multimedia/privacy/region1/enableenable/disable area
prefix/cameraid/status/multimedia/privacy/region1/colorset area color
prefix/cameraid/status/multimedia/privacy/region1/xoriginset X origin
prefix/cameraid/status/multimedia/privacy/region1/yoriginset Y origin
prefix/cameraid/status/multimedia/privacy/region1/heightset height
prefix/cameraid/status/multimedia/privacy/region1/widthset width

Flow Variables

So I need to add 6 MQTT-IN nodes for each area with the corresponding topics, attach a JSON node to them followed by a Change node to change the msg.payload.val (the value that the MQTT topic provided) to a flow variable:

  • flow.privacyregion1
  • flow.privacyregion1color
  • flow.privacyregion1xorigin
  • flow.privacyregion1yorigin
  • flow.privacyregion1height
  • flow.privacyregion1width

Test an API in Node-RED

Now we need to attach a Function node that writes those flow variables into the data structure that we defined in the Mock API Trial.

To get started let's try to retrieve one of our flow variables inside a function node:

msg.payload = '{"privacyregion1":"' + flow.get("privacyregion1") || 0 + '"}'

return msg;

Connect a Debug node to this function and you should be able to get the string "{"privacyregion1":"1"}" out whenever the node is triggered - or "{"privacyregion1":"0"}" if the privacy area is deactivated.

Data Structure

Now we have to add our data to the structure that we prepared earlier:

{
"getmultimediaattr": {
"getcover": {
"getcoverarea1": {
"show_1": "1",
"color_1": "0F42EB",
"x_1": "241",
"y_1": "240",
"w_1": "1434",
"h_1": "600"
},
"getcoverarea2": {
"show_2": "1",
"color_2": "E40C1E",
"x_2": "1680",
"y_2": "0",
"w_2": "237",
"h_2": "240"
},
"getcoverarea3": {
"show_3": "1",
"color_3": "0EEC4D",
"x_3": "0",
"y_3": "840",
"w_3": "240",
"h_3": "240"
},
"getcoverarea4": {
"show_4": "1",
"color_4": "F0EC14",
"x_4": "1680",
"y_4": "840",
"w_4": "240",
"h_4": "240"
}
}
}
}

So show_1 should hold the value from flow.get("privacyregion1") and so on. The Function node for this should look like this:

msg.payload = '{"getmultimediaattr":{"getcover":{"getcoverarea1":{"show_1":"' + flow.get("privacyregion1") + '","color_1":"' + flow.get("privacyregion1color") + '","x_1":"' + flow.get("privacyregion1xorigin") + '","y_1":"' + flow.get("privacyregion1yorigin") + '","w_1":"' + flow.get("privacyregion1width") + '","h_1":"' + flow.get("privacyregion1height") + '"},"getcoverarea2":{"show_2":"' + flow.get("privacyregion2") + '","color_2":"' + flow.get("privacyregion2color") + '","x_2":"' + flow.get("privacyregion2xorigin") + '","y_2":"' + flow.get("privacyregion2yorigin") + '","w_2":"' + flow.get("privacyregion2width") + '","h_2":"' + flow.get("privacyregion2height") + '"},"getcoverarea3":{"show_3":"' + flow.get("privacyregion3") + '","color_3":"' + flow.get("privacyregion3color") + '","x_3":"' + flow.get("privacyregion3xorigin") + '","y_3":"' + flow.get("privacyregion3yorigin") + '","w_3":"' + flow.get("privacyregion3width") + '","h_3":"' + flow.get("privacyregion3height") + '"},"getcoverarea4":{"show_4":"' + flow.get("privacyregion4") + '","color_4":"' + flow.get("privacyregion4color") + '","x_4":"' + flow.get("privacyregion4xorigin") + '","y_4":"' + flow.get("privacyregion4yorigin") + '","w_4":"' + flow.get("privacyregion4width") + '","h_4":"' + flow.get("privacyregion4height") + '"}}}}'

return msg;

Adding a JSON node to this Function node will provide us with a Javascript object with all our data in the structure that we desired - sweet!

Test an API in Node-RED

All I now have to do is to add a Change node to set the msg.payload to a global variable I will call multimedia:

Test an API in Node-RED

This way I am now able to query my data structure from anywhere and get the latest state from my camera.

GET State

To GET the state from our camera through our REST API we now have to create web hooks. I want the URLs to mirror the corresponding MQTT Topic they are querying:

/status/multimedia/privacy/
/status/multimedia/privacy/region1/
/status/multimedia/privacy/region1/enable/
/status/multimedia/privacy/region1//color/
/status/multimedia/privacy/region1/xorigin/
/status/multimedia/privacy/region1/yorigin/
/status/multimedia/privacy/region1/width/
/status/multimedia/privacy/region1/height/

When the user accesses the webhook, e.g. http://192.168.2.111:1880/status/multimedia/privacy/ it has to trigger a Function node that gets the information out of our data structure:

const state = global.get("multimedia");
var string = JSON.stringify(state);
var output = JSON.parse(string);
msg.payload = output.getmultimediaattr.getcover;
return msg;

This will retrieve all the information on Privacy Masks from our camera state. While the following function will only retrieve one key value - is the privacy mask 1 enabled or not:

const state = global.get("multimedia");
var string = JSON.stringify(state);
var output = JSON.parse(string);
msg.payload = output.getmultimediaattr.getcover.getcoverarea1.show_1;
return msg;

Test an API in Node-RED

You can test these URLs with your browser:

Test an API in Node-RED

Node-RED Flow

[{"id":"374a177d.707f68","type":"http in","z":"74640aeb.4a0fa4","name":"/status/multimedia/privacy/","url":"/status/multimedia/privacy/","method":"get","upload":false,"swaggerDoc":"","x":963,"y":151,"wires":[["a64d0836.789d38"]]},{"id":"ed693714.22a568","type":"http response","z":"74640aeb.4a0fa4","name":"","statusCode":"","headers":{},"x":1428,"y":151,"wires":[]},{"id":"a64d0836.789d38","type":"function","z":"74640aeb.4a0fa4","name":"GET multimedia","func":"const state = global.get(\"multimedia\");\nvar string = JSON.stringify(state);\nvar output = JSON.parse(string);\nmsg.payload = output.getmultimediaattr.getcover;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1262,"y":151,"wires":[["ed693714.22a568"]]}]

SET State: URL Queries

To SET a state we need to create another set of webhooks that allows us to send data in. Just like with the MQTT Topics I am going to use the same URL structure - I am just leaving the /status/ part out:

Test an API in Node-RED

Those URLs are supposed to carry the required data as URL queries. The example below shows the complete URL to set the state of all 4 privacy areas. The data is added as key/value pairs at the end of the URL:

http://192.168.2.111:1880/multimedia/privacy/?getcoverarea1_show_1=0&getcoverarea1_color_1=0F42EB&getcoverarea1_x_1=240&getcoverarea1_y_1=240&getcoverarea1_w_1=1434&getcoverarea1_h_1=600&getcoverarea2_show_2=0&getcoverarea2_color_2=E40C1E&getcoverarea2_x_2=1680&getcoverarea2_y_2=0&getcoverarea2_w_2=237&getcoverarea2_h_2=240&getcoverarea3_show_3=0&getcoverarea3_color_3=0EEC4D&getcoverarea3_x_3=0&getcoverarea3_y_3=840&getcoverarea3_w_3=240&getcoverarea3_h_3=240&getcoverarea4_show_4=0&getcoverarea4_color_4=F0EC14&getcoverarea4_x_4=1680&getcoverarea4_y_4=840&getcoverarea4_w_4=240&getcoverarea4_h_4=240

The function node to extract those queries looks like this:

msg.payload = msg.req.query;
return msg;

I am going to return an HTML page that displays the data that has been sent into the flow:

Test an API in Node-RED

The HTML code to achieve this looks like this:

<html>
<head></head>
<body>
<h2>Privacy Area 1:</h2>
<ul>
<li><strong>Enabled:</strong> <span style="color:red;">{{req.query.getcoverarea1_show_1}}</span></li>
<li><strong>Color:</strong> <span style="color:red;">{{req.query.getcoverarea1_color_1}}</span></li>
<li><strong>X Origin:</strong> <span style="color:red;">{{req.query.getcoverarea1_x_1}}</span></li>
<li><strong>Y Origin:</strong> <span style="color:red;">{{req.query.getcoverarea1_y_1}}</span></li>
<li><strong>Width:</strong> <span style="color:red;">{{req.query.getcoverarea1_w_1}}</span></li>
<li><strong>Height:</strong> <span style="color:red;">{{req.query.getcoverarea1_h_1}}</span></li>
</ul>
<h2>Privacy Area 2:</h2>
<ul>
<li><strong>Enabled:</strong> <span style="color:red;">{{req.query.getcoverarea2_show_2}}</span></li>
<li><strong>Color:</strong> <span style="color:red;">{{req.query.getcoverarea2_color_2}}</span></li>
<li><strong>X Origin:</strong> <span style="color:red;">{{req.query.getcoverarea2_x_2}}</span></li>
<li><strong>Y Origin:</strong> <span style="color:red;">{{req.query.getcoverarea2_y_2}}</span></li>
<li><strong>Width:</strong> <span style="color:red;">{{req.query.getcoverarea2_w_2}}</span></li>
<li><strong>Height:</strong> <span style="color:red;">{{req.query.getcoverarea2_h_2}}</span></li>
</ul>
<h2>Privacy Area 3:</h2>
<ul>
<li><strong>Enabled:</strong> <span style="color:red;">{{req.query.getcoverarea3_show_3}}</span></li>
<li><strong>Color:</strong> <span style="color:red;">{{req.query.getcoverarea3_color_3}}</span></li>
<li><strong>X Origin:</strong> <span style="color:red;">{{req.query.getcoverarea3_x_3}}</span></li>
<li><strong>Y Origin:</strong> <span style="color:red;">{{req.query.getcoverarea3_y_3}}</span></li>
<li><strong>Width:</strong> <span style="color:red;">{{req.query.getcoverarea3_w_3}}</span></li>
<li><strong>Height:</strong> <span style="color:red;">{{req.query.getcoverarea3_h_3}}</span></li>
</ul>
<h2>Privacy Area 4:</h2>
<ul>
<li><strong>Enabled:</strong> <span style="color:red;">{{req.query.getcoverarea4_show_4}}</span></li>
<li><strong>Color:</strong> <span style="color:red;">{{req.query.getcoverarea4_color_4}}</span></li>
<li><strong>X Origin:</strong> <span style="color:red;">{{req.query.getcoverarea4_x_4}}</span></li>
<li><strong>Y Origin:</strong> <span style="color:red;">{{req.query.getcoverarea4_y_4}}</span></li>
<li><strong>Width:</strong> <span style="color:red;">{{req.query.getcoverarea4_w_4}}</span></li>
<li><strong>Height:</strong> <span style="color:red;">{{req.query.getcoverarea4_h_4}}</span></li>
</ul>
</body>
</html>

Node-RED Flow

[{"id":"ce7bdbd2.edc318","type":"function","z":"74640aeb.4a0fa4","name":"extract query","func":"msg.payload = msg.req.query;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1174,"y":1411,"wires":[["291a2bfa.b1bc44","5e2cfeb7.6f634"]]},{"id":"d31d7d91.6917f","type":"http in","z":"74640aeb.4a0fa4","name":"/multimedia/privacy/","url":"/multimedia/privacy/","method":"get","upload":false,"swaggerDoc":"","x":936,"y":1411,"wires":[["ce7bdbd2.edc318"]]},{"id":"85ed312b.cfb2b","type":"http response","z":"74640aeb.4a0fa4","name":"","x":1434,"y":1411,"wires":[]},{"id":"291a2bfa.b1bc44","type":"template","z":"74640aeb.4a0fa4","name":"page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n    <head></head>\n    <body>\n        <h2>Privacy Area 1:</h2>\n        <ul>\n            <li><strong>Enabled:</strong> <span style=\"color:red;\">{{req.query.getcoverarea1_show_1}}</span></li>\n            <li><strong>Color:</strong> <span style=\"color:red;\">{{req.query.getcoverarea1_color_1}}</span></li>\n            <li><strong>X Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea1_x_1}}</span></li>\n            <li><strong>Y Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea1_y_1}}</span></li>\n            <li><strong>Width:</strong> <span style=\"color:red;\">{{req.query.getcoverarea1_w_1}}</span></li>\n            <li><strong>Height:</strong> <span style=\"color:red;\">{{req.query.getcoverarea1_h_1}}</span></li>\n        </ul>\n        <h2>Privacy Area 2:</h2>\n        <ul>\n            <li><strong>Enabled:</strong> <span style=\"color:red;\">{{req.query.getcoverarea2_show_2}}</span></li>\n            <li><strong>Color:</strong> <span style=\"color:red;\">{{req.query.getcoverarea2_color_2}}</span></li>\n            <li><strong>X Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea2_x_2}}</span></li>\n            <li><strong>Y Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea2_y_2}}</span></li>\n            <li><strong>Width:</strong> <span style=\"color:red;\">{{req.query.getcoverarea2_w_2}}</span></li>\n            <li><strong>Height:</strong> <span style=\"color:red;\">{{req.query.getcoverarea2_h_2}}</span></li>\n        </ul>\n        <h2>Privacy Area 3:</h2>\n        <ul>\n            <li><strong>Enabled:</strong> <span style=\"color:red;\">{{req.query.getcoverarea3_show_3}}</span></li>\n            <li><strong>Color:</strong> <span style=\"color:red;\">{{req.query.getcoverarea3_color_3}}</span></li>\n            <li><strong>X Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea3_x_3}}</span></li>\n            <li><strong>Y Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea3_y_3}}</span></li>\n            <li><strong>Width:</strong> <span style=\"color:red;\">{{req.query.getcoverarea3_w_3}}</span></li>\n            <li><strong>Height:</strong> <span style=\"color:red;\">{{req.query.getcoverarea3_h_3}}</span></li>\n        </ul>\n        <h2>Privacy Area 4:</h2>\n        <ul>\n            <li><strong>Enabled:</strong> <span style=\"color:red;\">{{req.query.getcoverarea4_show_4}}</span></li>\n            <li><strong>Color:</strong> <span style=\"color:red;\">{{req.query.getcoverarea4_color_4}}</span></li>\n            <li><strong>X Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea4_x_4}}</span></li>\n            <li><strong>Y Origin:</strong> <span style=\"color:red;\">{{req.query.getcoverarea4_y_4}}</span></li>\n            <li><strong>Width:</strong> <span style=\"color:red;\">{{req.query.getcoverarea4_w_4}}</span></li>\n            <li><strong>Height:</strong> <span style=\"color:red;\">{{req.query.getcoverarea4_h_4}}</span></li>\n        </ul>\n    </body>\n</html>","x":1314,"y":1411,"wires":[["85ed312b.cfb2b"]]},{"id":"5e2cfeb7.6f634","type":"link out","z":"74640aeb.4a0fa4","name":"","links":["61156a83.7a2f24","524c5b29.ca8514","8d8e044a.e7c388","47fb417.82b56c","f817774d.e26c18","917e3fe8.f37d1","63b25053.969d2","2d440835.d51648","d784a8d6.dc5a48","3a9c985.1820768","8fe2845d.3fde28","c2c200bd.6f236","2e3e1bc.cd0dae4","585423f3.05ffac","7521ae6b.1c2f2","38f60db7.6b85a2","90715445.4d5bc8","5db3e480.ef5a1c","7eec1710.faa1e8","a985acc8.f3e7c","415f1ebf.82851","f3ff5f2f.be58f","6954e258.839cac","30066992.e22976"],"x":1519,"y":1411,"wires":[]}]

You can also break this down to a per-region level to configure a single privacy area instead of all of them at once:

http://192.168.2.111:1880/multimedia/privacy/region1/?getcoverarea1_show_1=0&getcoverarea1_color_1=0F42EB&getcoverarea1_x_1=240&getcoverarea1_y_1=240&getcoverarea1_w_1=1434&getcoverarea1_h_1=600
http://192.168.2.111:1880/multimedia/privacy/region2/?getcoverarea2_show_2=0&getcoverarea2_color_2=E40C1E&getcoverarea2_x_2=1680&getcoverarea2_y_2=0&getcoverarea2_w_2=237&getcoverarea2_h_2=240
http://192.168.2.111:1880/multimedia/privacy/region3/?getcoverarea3_show_3=0&getcoverarea3_color_3=0EEC4D&getcoverarea3_x_3=0&getcoverarea3_y_3=840&getcoverarea3_w_3=240&getcoverarea3_h_3=240
http://192.168.2.111:1880/multimedia/privacy/region4/?getcoverarea4_show_4=1&getcoverarea4_color_4=F0EC14&getcoverarea4_x_4=1680&getcoverarea4_y_4=840&getcoverarea4_w_4=240&getcoverarea4_h_4=240

But we are not done yet. We now have to connect each of those webhooks to an MQTT command topic to actually set the new values on our camera. For this I will set the message.payload to the query that I want to send to my camera (using a Change node). I then have to transform it into a valid MQTT payload using a Function node:

msg.payload = '{"val":"'+msg.payload+'"}';
return msg;

And send it into an MQTT Out node with the corresponding MQTT command topic:

Test an API in Node-RED

Node-RED Flow

[{"id":"e1ab971f.160e98","type":"mqtt out","z":"74640aeb.4a0fa4","name":"multimedia/privacy/region1/enable","topic":"cameras/mycamera1/multimedia/privacy/region1/enable","qos":"1","retain":"false","broker":"7553a41d.22b8bc","x":2241,"y":80,"wires":[]},{"id":"74dff1bb.d61ab","type":"function","z":"74640aeb.4a0fa4","name":"Transform","func":"msg.payload = '{\"val\":\"'+msg.payload+'\"}';\nreturn msg;","outputs":1,"noerr":0,"x":2021,"y":80,"wires":[["e1ab971f.160e98"]]},{"id":"bdc67dc9.41e3f","type":"change","z":"74640aeb.4a0fa4","name":"getcoverarea1_show_1","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.getcoverarea1_show_1","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1831,"y":80,"wires":[["74dff1bb.d61ab"]]},{"id":"7553a41d.22b8bc","type":"mqtt-broker","z":"","name":"192.168.2.117","broker":"192.168.2.117","port":"8883","tls":"c411fab7.e16228","clientid":"","usetls":true,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"c411fab7.e16228","type":"tls-config","z":"","name":"","cert":"","key":"","ca":"","certname":"mqtt_client-new.crt","keyname":"","caname":"","servername":"","verifyservercert":false}]

We are now able to read the state on our camera and to set it to a desired value. The one thing that is missing is the ability to set a single key value. Here I don't want to use URL Queries - you can, of course, if you like - but keep it more readable use URL Parameters instead.

SET State: URL Params

URL Parameters look like this:

http://192.168.2.111:1880/multimedia/privacy/region1/enable/1
http://192.168.2.111:1880/multimedia/privacy/region1/color/0F42EB
http://192.168.2.111:1880/multimedia/privacy/region1/xorigin/240
http://192.168.2.111:1880/multimedia/privacy/region1/yorigin/240
http://192.168.2.111:1880/multimedia/privacy/region1/width/1434
http://192.168.2.111:1880/multimedia/privacy/region1/height/600

The value I want to set is simply added to the URL and extracted by defining the structure inside our webhooks:

/multimedia/privacy/region1/enable/:param
/multimedia/privacy/region1/color/:param
/multimedia/privacy/region1/xorigin/:param
/multimedia/privacy/region1/yorigin/:param
/multimedia/privacy/region1/width/:param
/multimedia/privacy/region1/height/:param

Test an API in Node-RED

To work with the parameter we have to use a Function node, well - you could also use a Change node - anyway... the Function node looks like this:

msg.payload = msg.req.params.param;
return msg;

This can then be connected to the same MQTT Out node we created before to set the state on our camera.

Node-RED FLow

[{"id":"d905bcfd.e401b","type":"http response","z":"74640aeb.4a0fa4","name":"","x":1534,"y":1694,"wires":[]},{"id":"39533208.b7a8ee","type":"template","z":"74640aeb.4a0fa4","name":"page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n    <head></head>\n    <body>\n        <h2>Privacy Area 1</h2>\n        <ul>\n            <li><strong>Enabled:</strong> <span style=\"color:red;\">{{req.params.param}}</span></li>\n        </ul>\n    </body>\n</html>","x":1414,"y":1694,"wires":[["d905bcfd.e401b"]]},{"id":"96a612ee.4b325","type":"http in","z":"74640aeb.4a0fa4","name":"/multimedia/privacy/region1/enable/:param","url":"/multimedia/privacy/region1/enable/:param","method":"get","upload":false,"swaggerDoc":"","x":1007,"y":1694,"wires":[["d1f31021.b62a8"]]},{"id":"d1f31021.b62a8","type":"function","z":"74640aeb.4a0fa4","name":"extract param","func":"msg.payload = msg.req.params.param;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1269,"y":1694,"wires":[["39533208.b7a8ee","6bcc3077.1719d"]]},{"id":"6bcc3077.1719d","type":"link out","z":"74640aeb.4a0fa4","name":"","links":["d1219254.69c7b"],"x":1610,"y":1695,"wires":[]}]

You can now try to switch your camera's privacy area 1 on and off by visiting the corresponding webhook using the correct parameter:

http://192.168.2.111:1880/multimedia/privacy/region1/enable/1
http://192.168.2.111:1880/multimedia/privacy/region1/enable/0