Friday, January 26, 2018

Controlling two fans

Here is another solution that I provided in Node-RED group. The solution is very similar to the one proposed in this topic (click here).



The requirements:

Use Node-RED to control two fans and publish the status of both fans to an MQTT broker.
Each fan should be activated for a given time (configurable) and should never be activated together. A base time should be defined (one minute or one hour for instance) and each fan should run for a slice of this base time. It should be given a break (deadtime) after fan1 is powered off and before fan2 is activated.

I tested with base time of 60 seconds, fan1 rotating for 30 seconds, fan2 rotating for 20 seconds and therefore with a dead-time (no fans rotating) of 10 seconds. The timing is fully configurable.


Proposed Solution:

It was implemented a cycling engine, inside a function node, that generates the commands to activate and deactivate each fan. The output of this function node also feeds and MQTT node that will publish the fan status.

A variable in the global context is used to store the user command to stop the flow. 

The activation commands are the sent to a function node (named Test Stop) that will check if there is a pending request to stop the fans. This way the shutdown of the fans is tested each time there a new control command is created. Therefore the fan will never stop during its turn. It will first end the rotation time already commanded.

The running time for each fan is guaranteed by the delay node. Each fan has a different (configurable) timing that is propagated in a property in the msg object.

Since the control commands are the properties msg.fan1 and msg.fan2 the actual fans can be controlled by any hardware that can read these properties. It possible to control remote fans without any change in the Node-RED flow if the fan controllers are able to subscribe to the MQTT broker.






Debug in Node-RED window:





Monitoring the status in an MQTT client that subscribed to the topics:





Flow:

[{"id":"7f2e3791.ab9be8","type":"function","z":"f6b8ce66.d6b76","name":"Cycle next fan","func":"switch (msg.next) {\n    \n    case 1:\n        msg.delay = msg.runtime1;\n        msg.fan1=\"ON\"; \n        msg.fan2=\"OFF\"; \n        msg.next = 2;\n        break;\n    case 2:\n        msg.delay = msg.deadtime;\n        msg.fan1=\"OFF\"; \n        msg.fan2=\"OFF\";\n        msg.next = 3;\n        break;  \n    case 3:\n        msg.delay = msg.runtime2;\n        msg.fan1=\"OFF\"; \n        msg.fan2=\"ON\";\n        msg.next = 1;\n        break; \n        \n    case 4:\n        msg.fan1=\"OFF\"; \n        msg.fan2=\"OFF\";\n        msg.reset = true;\n        break; \n}\n\n\n    msg1={topic:\"cmnd/fan1\", payload:msg.fan1};\n    msg2={topic:\"cmnd/fan2\", payload:msg.fan2};\n\n    // msg1 and msg2 are sent sequencially in outupt 1\n    // msg is sent on output 2 \n    return [[msg1, msg2],msg];","outputs":"2","noerr":0,"x":260,"y":220,"wires":[["92ce9449.6d90c8"],["e8228ca9.a66ce"]]},{"id":"fc7be0f.25b612","type":"inject","z":"f6b8ce66.d6b76","name":"Setup FANs","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":110,"y":80,"wires":[["54fbfa4f.909784"]]},{"id":"92ce9449.6d90c8","type":"mqtt out","z":"f6b8ce66.d6b76","name":"","topic":"","qos":"2","retain":"false","broker":"c7f4b651.bda738","x":530,"y":100,"wires":[]},{"id":"92b1e0bb.85b37","type":"debug","z":"f6b8ce66.d6b76","name":"","active":true,"console":"false","complete":"true","x":330,"y":380,"wires":[]},{"id":"81c129af.1fc868","type":"inject","z":"f6b8ce66.d6b76","name":"Stop FAN","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":160,"y":440,"wires":[["c56a07d9.6b00c8"]]},{"id":"c56a07d9.6b00c8","type":"function","z":"f6b8ce66.d6b76","name":"Command Stop","func":"global.set(\"StopFan\",true);\nreturn msg;","outputs":1,"noerr":0,"x":385.00000381469727,"y":441,"wires":[[]]},{"id":"e8228ca9.a66ce","type":"function","z":"f6b8ce66.d6b76","name":"Test Stop","func":"//  user commanded stopping fans\nif (global.get(\"StopFan\")) msg.next = 4;\nreturn msg;","outputs":1,"noerr":0,"x":160,"y":340,"wires":[["9f0d2faf.3d214","92b1e0bb.85b37"]]},{"id":"54fbfa4f.909784","type":"function","z":"f6b8ce66.d6b76","name":"Initialize","func":"// Define basetime as 60 seconds (one minute)\nmsg.basetime = 60*1000;\n\n// Define runtime1 (30 seconds)\nmsg.runtime1 = 30*1000;\n\n// Define runtime2 (20 seconds)\nmsg.runtime2 = 20*1000;\n\n// Define deadtime (basetime-runtimes\nmsg.deadtime = msg.basetime-msg.runtime1-msg.runtime2;\n\n// Next fan to be activated is number 1\nmsg.next = 1;\n\n//Stop criteria is false\nglobal.set(\"StopFan\",false);\n\nreturn msg;","outputs":1,"noerr":0,"x":260,"y":80,"wires":[["7f2e3791.ab9be8"]]},{"id":"9f0d2faf.3d214","type":"delay","z":"f6b8ce66.d6b76","name":"","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":340,"y":340,"wires":[["7f2e3791.ab9be8"]]},{"id":"c7f4b651.bda738","type":"mqtt-broker","z":"","broker":"broker.mqttdashboard.com","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"willTopic":"","willQos":"0","willPayload":"","birthTopic":"","birthQos":"0","birthPayload":""}]

Saturday, January 20, 2018

Manipulating JSON - Example 1

This was a request for help posted in Node-RED group.


Requirement:

To write a function that from message below return  different messages with the keys as topics (Time,Uptime, etc..) and as payload the value of each key.


Message provided:



Desired outcome:





















Proposed solution:

A code inside a function node that will iterate over all the keys in the original payload and will create an array (named output) that will contain objects with properties that complies with the requirement: {topic:key, payload:value}. As a reminder note that when a function node has only one output and the returned object is an array then several messages will be sent over the output (one for each element of the array).



// Declare an array to hold the objects
var output = []; 

var x;
// var x will hold each key from the msg.payload
for (x in msg.payload) 

// Store in the array output the objects, except for key = Wifi
{
    if (x != "Wifi") 
    {
    output.push({topic:x, payload:msg.payload[x]}); 
    }
}


// Store in the array output the objects  for key = Wifi
for (x in msg.payload.Wifi)
{
    output.push({topic:x, payload:msg.payload.Wifi[x]}); 
}
    
// below return statement will return a message for each element of the array
return [output];



Flow:


Notice that for the purpose of testing the logic I stored in the inject node the testing object as a JSON  structure. The inject node returns this JSON structure as a Javascript object (inside msg.payload), therefore no need to use the JSON node to convert.


{
    "Time": "2017-12-26T22:39:08",
    "Uptime": 4,
    "Vcc": 3.25,
    "POWER": "ON",
    "Wifi": {
        "AP": 1,
        "SSId": "JAZZTEL_david",
        "RSSI": 72,
        "APMac": "20:89:86:1E:31:E2"
    }
}



Remarks:

This user case shows that transforming JSON structure is a common need. An alternative approach would be using JSONata expression (along with a Change node) but we respected the specific requirement to build this flow using a function node.

It would be interesting to generalize this flow to perform the same operation regardless of the amount of keys like "Wifi" in the original message.



Flow to import into Node-RED:


[{"id":"f2a5e861.443a28","type":"debug","z":"97920a03.8f8d28","name":"","active":true,"console":"false","complete":"true","x":470,"y":120,"wires":[]},{"id":"cbb69cda.02a22","type":"inject","z":"97920a03.8f8d28","name":"","topic":"","payload":"{\"Time\":\"2017-12-26T22:39:08\",\"Uptime\":4,\"Vcc\":3.25,\"POWER\":\"ON\",\"Wifi\":{\"AP\":1,\"SSId\":\"JAZZTEL_david\",\"RSSI\":72,\"APMac\":\"20:89:86:1E:31:E2\"}}","payloadType":"json","repeat":"","crontab":"","once":false,"x":170,"y":120,"wires":[["8a4b3666.734578"]]},{"id":"8a4b3666.734578","type":"function","z":"97920a03.8f8d28","name":"Parse payload","func":"var x;\n// Declare an array to hold the objects\nvar output = []; \n\n// var x will hold each key from the msg.payload\nfor (x in msg.payload) \n\n// Store in the array output the objects, except for key = Wifi\n{\n    if (x != \"Wifi\") \n    {\n    output.push({topic:x, payload:msg.payload[x]}); \n    }\n}\n\n\n// Store in the array output the objects  for key = Wifi\nfor (x in msg.payload.Wifi)\n{\n    output.push({topic:x, payload:msg.payload.Wifi[x]}); \n}\n    \n// below retunr statement will return a message for each element of the array\nreturn [output];","outputs":1,"noerr":0,"x":320,"y":120,"wires":[["f2a5e861.443a28"]]}]

CHANGE node - Part 6 - Final - Takeaways

Summary of key points when using the Change node:


work in progress. Will be published sooner.

Wednesday, January 3, 2018

CHANGE node - Part 5 - Rule Set

The SET rule from the Change node allows you to create a new property (key, value) or set (change) the value of an existing property.

The value to be set can be taken from a variety of different sources: a value from another object, a hard-coded string, number, buffer or boolean, a JSON object, a timestamp or from the result of a  JSONata expression.

This makes the Change rule to be very useful.

Be careful: important

There will be always two fields in the configuration dialog of the Set rule.

The first one, side by side with the rule name Set is the recipient of the action.

The second field is from where Node-RED will take the new value. I rather prefer to think of the name of this field as "Taken from " instead of "to". Some old school guys may take a wrong guess and think this dialog page is something like "Copy first field to the second field". This would be a mistake. It should be understood as "Store in the path given in the first field the value taken from the second field".







Example #1
Change the value of an existing object, in this case: msg.payload.ms1.father




After the rule is applied:






Example #2
Create a new property: msg.payload.msg1.uncle and set its value to "Uri"




Node-RED msg object after the rule is applied:







Example #3
Just to reinforce how to understand the dialog page from the rule Set: Store in the first field the value taken from the second field.





Before the rule is applied:





After the rule is applied:





Example #4
It is possible to use the rule to store properties in the flow context (or global context).

We expect that below rule stores in the property flow.payload the value taken from the second field.


It probably happened but it is not possible to check the end result with a debug node (it will not show the flow object). We could add an auxiliary function node just to display (using the statement node.warn) the content of the object flow. Instead, I will add a second rule to create a new property in the object msg (easy to check the full content of the msg object with the debug node) with the value taken from the flow object.




The second rule will store in msg.payload.msg5 the property taken from flow.payload


Resulting:




Example #5
The change node can be used to inject in the flow a large JSON object.

Let´s say you need to inject the following data into your flow:

[
    {
        "_id": "58e39679e4b024931d65c440",
        "ap": "80:2a:a8:c0:b7:d3",
        "channel": "44",
        "datetime": "2017-04-04T12:49:47Z",
        "hostname": "android-2c54f069416770eb",
        "key": "EVT_WU_Connected",
        "msg": "User[e8:50:8b:a5:83:39] has connected to AP[80:2a:a8:c0:b7:d3] with ssid \"ssid\" on \"channel 44(na)\"",
        "radio": "na",
        "site_id": "58a18811e4b0f7d0d14435bc",
        "ssid": "ssid",
        "subsystem": "wlan",
        "time": 1491310187878,
        "user": "e8:00:00:00:00:01"
    },
    {
        "_id": "58e39679e4b024931d65c440",
        "ap": "80:2a:a8:c0:b7:d3",
        "channel": "44",
        "datetime": "2017-04-04T12:49:47Z",
        "hostname": "android-2c54f069416770eb",
        "key": "EVT_WU_Disconnected",
        "msg": "User[e8:50:8b:a5:83:39] has connected to AP[80:2a:a8:c0:b7:d3] with ssid \"ssid\" on \"channel 44(na)\"",
        "radio": "na",
        "site_id": "58a18811e4b0f7d0d14435bc",
        "ssid": "ssid",
        "subsystem": "wlan",
        "time": 1491310187878,
        "user": "e8:00:00:00:00:02"
    },
    {
        "_id": "58e39679e4b024931d65c440",
        "ap": "80:2a:a8:c0:b7:d3",
        "channel": "44",
        "datetime": "2017-04-04T12:49:47Z",
        "hostname": "android-2c54f069416770eb",
        "key": "EVT_WU_Disconnected",
        "msg": "User[e8:50:8b:a5:83:39] has connected to AP[80:2a:a8:c0:b7:d3] with ssid \"ssid\" on \"channel 44(na)\"",
        "radio": "na",
        "site_id": "58a18811e4b0f7d0d14435bc",
        "ssid": "ssid",
        "subsystem": "wlan",
        "time": 1491310187878,
        "user": "e8:00:00:00:00:03"
    },
    {
        "_id": "58e39679e4b024931d65c440",
        "ap": "80:2a:a8:c0:b7:d3",
        "channel": "44",
        "datetime": "2017-04-04T12:49:47Z",
        "hostname": "android-2c54f069416770eb",
        "key": "EVT_WU_Connected",
        "msg": "User[e8:50:8b:a5:83:39] has connected to AP[80:2a:a8:c0:b7:d3] with ssid \"ssid\" on \"channel 44(na)\"",
        "radio": "na",
        "site_id": "58a18811e4b0f7d0d14435bc",
        "ssid": "ssid",
        "subsystem": "wlan",
        "time": 1491310187878,
        "user": "e8:00:00:00:00:04"
    },
    {
        "_id": "58e39679e4b024931d65c440",
        "ap": "80:2a:a8:c0:b7:d3",
        "channel": "44",
        "datetime": "2017-04-04T12:49:47Z",
        "hostname": "android-2c54f069416770eb",
        "key": "EVT_WU_Connected",
        "msg": "User[e8:50:8b:a5:83:39] has connected to AP[80:2a:a8:c0:b7:d3] with ssid \"ssid\" on \"channel 44(na)\"",
        "radio": "na",
        "site_id": "58a18811e4b0f7d0d14435bc",
        "ssid": "ssid",
        "subsystem": "wlan",
        "time": 1491310187878,
        "user": "e8:00:00:00:00:05"
    }
]



You can do it by using the SET option from the change node.









Tuesday, January 2, 2018

CHANGE node - Part 4 - Rule Change

The change rule from the change node is quite powerful and flexible.

It will always work on the value side of the key/value pair.

The rule will search for a pattern in the property value. If this pattern is matched then the value will be replaced by what you configure in the field "Replace with".

The "Replace with" value could be any hard-coded value (string, boolean, number, buffer) or any value from any other property from msg or flow or global objects.

You can imagine that the Change rule is like a powerful conditional statement that will check if the value of a property (configured in the field change)  matches the value from the field "Search for". If the match occurs then the value configured in the last field (Replace with) will be assigned to the original property.









Example #1: 
Change property  msg.payload.msg1.father to value "Frederic" if there is a character "f" inside the value of  msg.payload.msg1.father






Result:







Example #2: 
Change property  msg.payload.msg1.mother to value "Mary" if there is any character inside the value of  msg.payload.msg1.mother.

In this case, we can use a regular expression to search and check if there is a match.

Notice that the regular expression in the field "Search for" is just a dot. This elementary regular expression will result in a positive match if any character exists in the field "Search for".






Node-RED msg object before the rule:





Node-RED msg object after the rule:







Example #3: 
Change property  msg.payload.msg1.sister to equal whatever is the value of msg.payload.msg1.mother.


Taking advantage of the previous example I will add an additional change rule. I remind you that the order of the rules matter. The rules are verified in the sequence they appear in the configuration panel. By the way, you can easily change the order of the rules with a drag and drop operation. The drag and drop can be performed when you hover the mouse pointer on the icon with 3 lines that exist in front of the field "Search for".




Result: