Testing a new REST API in Node-RED
- MQTT Intermediate
- Flow Variables
- Data Structure
- GET State
- SET State: URL Queries
- SET State: URL Params
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:
- INSTAR MQTT API Overview
- List of all available MQTT Topics
- Guide on how to interact with the MQTT Interface using Node-RED
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 Topic | Description |
---|---|
prefix/cameraid/status/multimedia/privacy/region1/enable | enable/disable area |
prefix/cameraid/status/multimedia/privacy/region1/color | set area color |
prefix/cameraid/status/multimedia/privacy/region1/xorigin | set X origin |
prefix/cameraid/status/multimedia/privacy/region1/yorigin | set Y origin |
prefix/cameraid/status/multimedia/privacy/region1/height | set height |
prefix/cameraid/status/multimedia/privacy/region1/width | set 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
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!
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
:
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;
You can test these URLs with your browser:
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:
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:
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:
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
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