How to store data in context hierarchichally

This is probably a really basic question.

This seems to work fine:

global.set("WaterSupplyUnit.LeakZone1",true)

An expandable object appears in context explorer, with "LeakZone1" as an available subvalue.

But if I want to store "sub sub values" like this:

global.set("WaterSupplyUnit.LeakZone1.leak", true)
global.set("WaterSupplyUnit.LeakZone1.leakdate", new Date().toLocaleDateString())
global.set("WaterSupplyUnit.LeakZone1.leaktime", new Date().toLocaleTimeString())

the values are not stored.

So it seems I can go "one deep" but no further.

Obvs I'm doing something wrong here!

1 Like

It is easier to do something like:

var dt = new Date().toLocaleDateString()
var WaterSupplyUnit = {
    LeakZone1: {
        leak: true,
        leakdate: dt,
        leaktime: dt,
    }
}
global.set("WaterSupplyUnit", WaterSupplyUnit)
1 Like

Thanks for the advice. Will do that. Also it's a bit clearer to read.

If you have already set WaterSupplyUnit.LeakZone1 to a Boolean type as per your first example, you cannot set sub properties of it. Only Objects can have properties.

1 Like

[EDITED]

So I've cleared the WaterSupplyUnit object (by deleting in context explorer) as Nick implies I should.

But with the snippet from @TotallyInformation, I am getting a different problem

It now deletes values from other zones when I post a given zone.

I think I know why (I'm writing the whole object), but what's the best way to not overwrite the whole object in this case?

var d = new Date().toLocaleDateString()
var t = new Date().toLocaleTimeString()
var WaterSupplyUnit

switch (msg.payload) {
    case "ZONE 1 LEAK = TRUE" :
    WaterSupplyUnit = { LeakZone1: { leak: true, leakdate: d, leaktime: t, } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)
    break;

    case "ZONE 2 LEAK = TRUE" :
    WaterSupplyUnit = { LeakZone2: { leak: true, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;

    case "ZONE 3 LEAK = TRUE" :
    WaterSupplyUnit = { LeakZone3: { leak: true, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;

    case "ZONE 4 LEAK = TRUE" :
    WaterSupplyUnit = { LeakZone4: { leak: true, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;
    
    case "ZONE 1 LEAK = FALSE" :
    WaterSupplyUnit = { LeakZone1: { leak: false, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;
    
    case "ZONE 2 LEAK = FALSE" :
    WaterSupplyUnit = { LeakZone2: { leak: false, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;

    case "ZONE 3 LEAK = FALSE" :
    WaterSupplyUnit = { LeakZone3: { leak: false, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;

    case "ZONE 4 LEAK = FALSE" :
    WaterSupplyUnit = { LeakZone4: { leak: false, leakdate: d, leaktime: t } }
    global.set("WaterSupplyUnit", WaterSupplyUnit)    
    break;

}
return msg;

Get it...
var currentValue = global.get("WaterSupplyUnit") || {};

Update it...
currentValue.this = that;

Store it...
global.set

Point one: The pattern that Steve mentioned has to be respected. When updating context we get the context, update it and store it back.

Point two: The general idea seems to be: create an updated object and then use it to update the main object.

Based on your first post and further comments it could be done with this function:

let pay = global.get("WaterSupplyUnit" || {});


function updateStatus() { 
    let ustatus = {};
    ustatus.leak = true;
    ustatus.leakdate = new Date().toLocaleDateString();
    ustatus.leaktime = new Date().toLocaleTimeString();
    return ustatus;
}

switch (msg.payload) {
    case "ZONE 1 LEAK = TRUE" :
    global.set("WaterSupplyUnit.LeakZone1", updateStatus());
    break;

    case "ZONE 2 LEAK = TRUE" :
    global.set("WaterSupplyUnit.LeakZone2", updateStatus());    
    break;

    case "ZONE 3 LEAK = TRUE" :
    global.set("WaterSupplyUnit.LeakZone3", updateStatus());   
    break;

    case "ZONE 4 LEAK = TRUE" :
    global.set("WaterSupplyUnit.LeakZone4", updateStatus());   
    break;
}
    
    return msg;

Edit: first line is useless.. and I am not respecting point 1, :joy:

Now I have to understand why it works, :rofl: but only after the lunch...

I remember now.. this is a behaviour that is not intutive (but nice).

Javascript will not be happy with below code. Of course I can not assign a value to property c given that b is undefined.

let a = {};
a.b.c = "testing";
return msg;

Node-RED (global.set or the switch node) will happily accept an statement like below, even if a and b are not defined upfront:

global.set("a.b.c", "123");
return msg;

Hi, firstly, what generates the payload "ZONE 1 LEAK = TRUE"?
If you do, then I'd suggest a simplification e.g. send msg.leak = true and msg.zone = 1

Anyhow, try this...

  var d = new Date().toLocaleDateString()
  var t = new Date().toLocaleTimeString()  

  //get existing object from global
  var WaterSupplyUnit = global.get("WaterSupplyUnit") || {};
  
  //get the zone number and TRUE/FALSE
  var r = /ZONE (\d+) LEAK = (TRUE|FALSE)/gm
  var x = r.exec(msg.payload);
  var zone = x[1];//get ZONE number from regex result
  var tf = x[2] == "TRUE";  //get TRUE or FALSE from regex result
  
  WaterSupplyUnit["LeakZone" + zone] = { leak: tf, leakdate: d, leaktime: t } };

  //store object back into global
  global.set("WaterSupplyUnit",WaterSupplyUnit);

Point of note: This will update the date/time even if leak is false - you may wish to only update the leakdate if leak == true

Also, note, I have not considered what what will happen if the payload is not formatted correctly or if the regex fails (I'll let you handle that)

If you have lots (or an unknown number) of leakzones, and as the data is an object rather than an array, you can simplify processing and reduce code by using the object keys as an array.

//get existing object from global
var WaterSupplyUnit = global.get("WaterSupplyUnit") || {};

var keys = Object.keys(WaterSupplyUnit)
keys.forEach(function(unit, index) {
    ...
}

Hi @Steve-Mcl, @TotallyInformation, and @Andrei

I've always been a bit too lazy to think about objects properly, so thanks for kicking me on that one. I'll go with a hybrid of Steve and Julian's suggestion.

My arduino sends the message over MQTT. (So yes, me.) It's a bit of a stupid way to do it. In the past I've used codes like 0;1 meaning a leak detected on sensor 0, I should have done that. Will probably change it.

Hmm, well not really a good approach for JavaScript where everything is an object of some sort! :slight_smile:

In which case, I would suggest you build this object, complete with timestamp at source (on the Arduino) and send the object as JSON. Then at node-red side simply feed the MQTT JSON string into a JSON node to rebuild the object.

And the beauty of MQTT, you don't have to change your existing methods of sending a string, just add code to build the object and send it on a different topic meaning you have backwards compatibility.

I also think that the JavaScript switch statement generates a clumsy code as it get large.

Alternatively can use an object (always objects) to make the code simpler:

let dic = {
    "ZONE 1 LEAK = TRUE" : "WaterSupplyUnit.LeakZone1",
    "ZONE 2 LEAK = TRUE" : "WaterSupplyUnit.LeakZone2",
    "ZONE 3 LEAK = TRUE" : "WaterSupplyUnit.LeakZone3",
    "ZONE 4 LEAK = TRUE" : "WaterSupplyUnit.LeakZone4"
}

function updateStatus() { 
    let ustatus = {};
    ustatus.leak = true;
    ustatus.leakdate = new Date().toLocaleDateString();
    ustatus.leaktime = new Date().toLocaleTimeString();
    return ustatus;
}

let zone =  dic[msg.payload];
global.set(zone, updateStatus());
return msg;