Monday, February 6, 2017

Referencing nested array values in JavaScript from my Octoblu state device

Part 1: Use configuration events in Octoblu
Part 2: Creating custom devices in Octoblu
Part 3: Setting the state of an Octoblu device from a flow
Part 4: Listening to and acting on device state change in Octoblu
Part 5: Breaking values into new keys with a function node
Part 6: Reformatting nested JSON with JavaScript
Part 7: Logical data nesting with your Octoblu state device

Okay, here is the big post that I have spent an entire week working up to.

I have to admit, I don't write code every day and am self taught JavaScript (along with Python,  PowerShell, and batch) so this took me a while to work through.

From my last post, my incoming message looks this this:

{
  "msg": {
    "rooms": {
      "Redmond": {
        "lunch": {
          "motion": {
            "name": "Redmond_lunch_motion",
            "mapTitle": "Redmond",
            "room": "lunch",
            "device": "motion",
          },
          "refrigerator": {
            "name": "Redmond_lunch_refrigerator",
            "mapTitle": "Redmond",
            "room": "lunch",
            "device": "refrigerator",
          },
          "door": {
            "name": "Redmond_lunch_door",
            "mapTitle": "Redmond",
            "room": "lunch",
            "device": "door",
          }
        }
      }
    },
    "fromUuid": "d5b77d9b-aaf3-f089a7096ee0"
  },
  "node": "b5149300-9cbd-1f1b56e5d7bb"
}

There can be a variable number of devices per room, and a variable number of rooms per map, and a variable number of maps.  My nesting pattern above is rooms.map.room.devices

Now for the hard part.
I want to evaluate the differences between values of different devices, per room.
This ends up being a lesson in how values are referenced in arrays in JavaScript.

Before I move forward, I have abbreviated the above JSON to spare you scrolling.  There are additional fields, and these additional fields contain Date objects that I am interested in.  And these Dates are formatted as odd numbers which are actually Epoch Time.

So, to give you the full treatment, here is a real message:

{
  "msg": {
    "rooms": {
      "Redmond": {
        "lunch": {
          "motion": {
            "name": "Redmond_lunch_motion",
            "mapTitle": "Redmond",
            "room": "lunch",
            "device": "motion",
            "motion": false,
            "motion_updated_at": 1485539337.480442,
            "battery": 1,
            "battery_updated_at": 1485539337.480442,
            "tamper_detected": null,
            "tamper_detected_updated_at": null,
            "temperature": 21.666666666666668,
            "temperature_updated_at": 1485539337.480442,
            "motion_true": "N/A",
            "motion_true_updated_at": 1485539125.483463,
            "tamper_detected_true": null,
            "tamper_detected_true_updated_at": null,
            "connection": true,
            "connection_updated_at": 1485539337.480442,
            "agent_session_id": null,
            "agent_session_id_updated_at": null,
            "connection_changed_at": 1485175984.3230183,
            "motion_changed_at": 1485539337.480442,
            "motion_true_changed_at": 1485539125.483463,
            "temperature_changed_at": 1485529054.5705206
          },
          "refrigerator": {
            "name": "Redmond_lunch_refrigerator",
            "mapTitle": "Redmond",
            "room": "lunch",
            "device": "refrigerator",
            "opened": false,
            "opened_updated_at": 1485539969.6240845,
            "tamper_detected": false,
            "tamper_detected_updated_at": 1476739884.682764,
            "battery": 1,
            "battery_updated_at": 1485539969.6240845,
            "tamper_detected_true": "N/A",
            "tamper_detected_true_updated_at": 1476739866.2962902,
            "connection": true,
            "connection_updated_at": 1485539969.6240845,
            "agent_session_id": null,
            "agent_session_id_updated_at": null,
            "opened_changed_at": 1485539969.6240845
          },
          "door": {
            "name": "Redmond_lunch_door",
            "mapTitle": "Redmond",
            "room": "lunch",
            "device": "door",
            "opened": false,
            "opened_updated_at": 1485538007.9089093,
            "tamper_detected": null,
            "tamper_detected_updated_at": null,
            "battery": 1,
            "battery_updated_at": 1485538007.9089093,
            "tamper_detected_true": null,
            "tamper_detected_true_updated_at": null,
            "connection": true,
            "connection_updated_at": 1485538007.9089093,
            "agent_session_id": null,
            "agent_session_id_updated_at": null,
            "opened_changed_at": 1485538007.9089093
          }
        }
      }
    },
    "fromUuid": "d5b77d9b-aaf3-f089a7096ee0"
  },
  "node": "b5149300-9cbd-1f1b56e5d7bb"
}

Now, the output I am looking for is to take some of these sensor Date values and evaluate them between each of the three devices.  Such as: door-refrigerator, motion-door, motion-refrigerator and so on.

If these values were in the same part of the message, it would be really easy.  I could simply dot reverence the values and do the math.
But they are not.  Each sensor is in its own document, in an array.

Now, if you recall a few posts back, I have a naming convention and I am standardizing three of the names:  "door", "refrigerator", and "motion".  Those I am not allowing to change.  But the room can and the map can.

Recall, I began this exercise this with just an array of devices with values.  Processed them to group by a logical naming pattern, saved that to an Octoblu state device, and now I am further processing that into my actionable data which I can easily handle with Octoblu filters to handle alerting or whatever I want to do.

So, to get you to read to the end and not just steal my code here is the output that I am producing, per room.
This gives me a nice single document per room as output - I can pass that to a demultiplex node to break the rot array apart and evaluate each document. 

My output looks like this:

{
  "msg": [
    {
      "motion": "motion",
      "motionAt": 1485544607.3195794,
      "motionAtHuman": "2017-01-27T19:16:47.319Z",
      "mapTitle": "Redmond",
      "room": "lunch",
      "refrigerator": "refrigerator",
      "fridgeOpenedAt": 1485539969.6240845,
      "fridgeOpenedAtHuman": "2017-01-27T17:59:29.624Z",
      "door": "door",
      "doorOpenedAt": 1485538007.9089093,
      "doorOpenedAtHuman": "2017-01-27T17:26:47.908Z",
      "diffDoorsOpenedMinutes": 32,
      "diffDoorMotionMinutes": 109,
      "diffRefrigeratorMotionMinutes": 77,
      "sinceDoorOpenMinutes": 115,
      "sinceRefrigeratorOpenMinutes": 82,
      "sinceMotionMinutes": 5
    }
  ],
  "node": "98cb8680-a264-1b8483214e06"
}

Now, to end this long, long story the JavaScript is below.
What I tried to do was have an intuitive way to read the code and reference each level of the document arrays, so you could understand where you were in the hierarchy.

// array to output
var output = [];
for ( var map in (msg.rooms) ){
    for ( var room in msg.rooms[map] ){
        var doorOpenedAt;
        var fridgeOpenedAt;
        var motionAt;
        var roomOutput = {};
        for ( var sensor in msg.rooms[map][room]){
            switch ( msg.rooms[map][room][sensor].device ) {
                case "door":
                    doorOpenedAt = moment.unix(msg.rooms[map][room][sensor].opened_changed_at);
                    roomOutput.door = msg.rooms[map][room][sensor].device;
                    roomOutput.doorOpenedAt = msg.rooms[map][room][sensor].opened_changed_at;
                    roomOutput.doorOpenedAtHuman = doorOpenedAt;
                    break;
                case "refrigerator":
                    fridgeOpenedAt = moment.unix(msg.rooms[map][room][sensor].opened_changed_at);
                    roomOutput.refrigerator = msg.rooms[map][room][sensor].device;
                    roomOutput.fridgeOpenedAt = msg.rooms[map][room][sensor].opened_changed_at;
                    roomOutput.fridgeOpenedAtHuman = fridgeOpenedAt;
                    break;
                case "motion":
                    motionAt = moment.unix(msg.rooms[map][room][sensor].motion_true_changed_at);
                    roomOutput.motion = msg.rooms[map][room][sensor].device;
                    roomOutput.motionAt = msg.rooms[map][room][sensor].motion_true_changed_at;
                    roomOutput.motionAtHuman = motionAt;
                    break;
            } // close of switch
            roomOutput.mapTitle = msg.rooms[map][room][sensor].mapTitle;
            roomOutput.room = msg.rooms[map][room][sensor].room;
        }  // close of sensor
        roomOutput.diffDoorsOpenedMinutes = Math.abs(doorOpenedAt.diff(fridgeOpenedAt, 'minutes'));  //removing Math.abs will give a + - if the refrigerator opens and the door does not it will be negative
        roomOutput.diffDoorMotionMinutes = Math.abs(doorOpenedAt.diff(motionAt, 'minutes'));
        roomOutput.diffRefrigeratorMotionMinutes = Math.abs(fridgeOpenedAt.diff(motionAt, 'minutes'));
        roomOutput.sinceDoorOpenMinutes = moment().diff(doorOpenedAt, 'minutes');
        roomOutput.sinceRefrigeratorOpenMinutes = moment().diff(fridgeOpenedAt, 'minutes');
        roomOutput.sinceMotionMinutes = moment().diff(motionAt, 'minutes');       
        output.push(roomOutput);
    }  //close of room
} // close of map
return output;


Lots of leading up to this post.  But I like to expand folks' understanding along the way.
And I know we don't all tolerate long articles.

I can thank Tobias Kreidl for even getting me started on this series of posts.
He asked a simple question, and I had a final answer, but I wanted to tell the journey so that he understood how I got to where I did.
That leaves it up to you to take what you need.  That's just how I write and respond to questions.

No comments: