A Node-RED sub-flow for checking entity area

TL;DR
A Node-RED sub-flow that checks if an entity is part of an area(s) or not.
Click here to get to the JSON to import.

As I did some searching for how to best determine the area of an entity in Node-RED, I found lots of good info. However, none of them were exactly what I was looking for, or they felt like overkill for what I really needed. So, I took what I learned from the answers that I did find, and built my own small sub-flow that has worked really well for me. So, I thought that I’d share it here so that others can benefit as well. I’m still relatively new to HA, so forgive me (and please correct me) if my solution is a bit naive or crazy inefficient.

Problem

I wanted to have a reusable “component” that would allow me to make a decision based upon the area of an entity. I wanted the following to be available:

  1. Ability to provide a string or list of strings specifying which area(s) I expected the entity to belong (IE, an inclusion list).
  2. Ability to provide a string or list of strings specifying which area(s) I expected the entity to NOT belong (IE, an exclusion list).
  3. If an area is found for the entity, add that area information to the flow message so that it can be used later in the flow.

Solution

I ended up breaking this up into 3 sub-flows:

  • Is in area? (the primary sub-flow)
  • get entity area (obtains the area of an entity)
  • join msgs (merges the properties of multiple messages together into a single message)

You don’t need to have the multiple sub-flows, but I use the extra two sun-flows for other things as well, so I separated them. Plus, it helps make it easier to follow i think.

Input

One of the following must be accessible during the processing of the sub-flow in order for it to know which entity to check (the order I’ve listed these here also denotes order of precedence):

  • env.entity_id
  • msg.entity_id
  • msg.payload.entity_id

Output

The message that is output will contain two new properties if msg.area_id was not present before the sub-flow was run:

  • area_id
  • area_name

This allows you to not only check the area, but also use the area information later in the flow without needing to obtain it again.

JSON to Import:

[{"id":"e4fd27479d2a5271","type":"subflow","name":"join msgs","info":"","category":"","in":[{"x":50,"y":50,"wires":[{"id":"757b13d490ea8a12"}]}],"out":[{"x":330,"y":50,"wires":[{"id":"757b13d490ea8a12","port":0}]}],"env":[{"name":"expected_msg_count","type":"num","value":"2","ui":{"label":{"en-US":"Message Count"},"type":"input","opts":{"types":["num","env"]}}}],"meta":{},"color":"#00cc55","icon":"node-red/join.svg","status":{"x":210,"y":120,"wires":[{"id":"4969a8bea1a43fa5","port":0}]}},{"id":"757b13d490ea8a12","type":"function","z":"e4fd27479d2a5271","name":"mergeMsgs","func":"let msgs = flow.get('msgs');\nmsgs = (msgs ? [ ...msgs, msg ] : [ msg ]);\n\nconst expectedMsgCount = env.get('expected_msg_count');\n\nif (msgs.length < expectedMsgCount) {\n    flow.set('msgs', msgs);\n    node.status({ fill: \"yellow\", shape: \"ring\", text: `${msgs.length} of ${expectedMsgCount} messages received`});\n    return;\n}\n\nlet newMsg = {};\n\nmsgs.forEach((it) => {\n    newMsg = { ...newMsg, ...it };\n});\n\nflow.set('msgs', []);\n\nnode.status({ fill: \"green\", shape: \"dot\", text: `${expectedMsgCount} messages received - ${new Date().toISOString()}`});\n\nreturn newMsg;","outputs":1,"timeout":"0","noerr":0,"initialize":"","finalize":"","libs":[],"x":190,"y":50,"wires":[[]]},{"id":"4969a8bea1a43fa5","type":"status","z":"e4fd27479d2a5271","name":"","scope":null,"x":90,"y":120,"wires":[[]]},{"id":"63c69a20e33faa2a","type":"subflow","name":"get entity area","info":"","category":"home_assistant","in":[{"x":40,"y":90,"wires":[{"id":"029b505c8c0a8ab9"}]}],"out":[{"x":1180,"y":90,"wires":[{"id":"2cc1e60c29b6efb2","port":0}]}],"env":[],"meta":{},"color":"#0b99da","icon":"font-awesome/fa-info","status":{"x":1360,"y":150,"wires":[{"id":"2b74f012bcbcb2fb","port":0},{"id":"2d31877b7fb12dec","port":0}]}},{"id":"a0d00e9f237e2016","type":"api-render-template","z":"63c69a20e33faa2a","name":"Template: area_id","server":"6a0a0747.044898","version":0,"template":"","resultsLocation":"area_id","resultsLocationType":"msg","templateLocation":"template","templateLocationType":"msg","x":590,"y":60,"wires":[["a9859a0a9612be68"]]},{"id":"116a21aa170db508","type":"api-render-template","z":"63c69a20e33faa2a","name":"Template: area","server":"6a0a0747.044898","version":0,"template":"","resultsLocation":"area","resultsLocationType":"msg","templateLocation":"template","templateLocationType":"msg","x":580,"y":120,"wires":[["a9859a0a9612be68"]]},{"id":"ba5fae6c27317ab3","type":"change","z":"63c69a20e33faa2a","name":"","rules":[{"t":"set","p":"template","pt":"msg","to":"'{{area_id(\"' & entity_id & '\")}}'","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":60,"wires":[["a0d00e9f237e2016"]]},{"id":"f72bbca8c75ddac1","type":"change","z":"63c69a20e33faa2a","name":"","rules":[{"t":"set","p":"template","pt":"msg","to":"'{{area_name(\"' & entity_id & '\")}}'","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":120,"wires":[["116a21aa170db508"]]},{"id":"2cc1e60c29b6efb2","type":"change","z":"63c69a20e33faa2a","name":"","rules":[{"t":"delete","p":"template","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":90,"wires":[["2b74f012bcbcb2fb"]]},{"id":"2b74f012bcbcb2fb","type":"change","z":"63c69a20e33faa2a","name":"Set status","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"text\":\"{{area_id}} ({{area_name}})\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":1220,"y":150,"wires":[[]]},{"id":"2d31877b7fb12dec","type":"status","z":"63c69a20e33faa2a","name":"","scope":["6aac04ce89045c51","a0d00e9f237e2016","116a21aa170db508"],"x":80,"y":220,"wires":[[]]},{"id":"a9859a0a9612be68","type":"subflow:e4fd27479d2a5271","z":"63c69a20e33faa2a","name":"","x":790,"y":90,"wires":[["2cc1e60c29b6efb2"]]},{"id":"029b505c8c0a8ab9","type":"function","z":"63c69a20e33faa2a","name":"setEntityId","func":"msg.entity_id = (env.entity_id ?? msg.entity_id ?? msg.payload?.entity_id) \nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":90,"wires":[["ba5fae6c27317ab3","f72bbca8c75ddac1"]]},{"id":"fa5adcb011d2ee93","type":"subflow","name":"Is in area?","info":"","category":"","in":[{"x":50,"y":130,"wires":[{"id":"7568fd9a8c462da8"}]}],"out":[{"x":760,"y":100,"wires":[{"id":"443d06390ee2d306","port":0}]},{"x":780,"y":160,"wires":[{"id":"443d06390ee2d306","port":1}]}],"env":[{"name":"area_ids","type":"str","value":"","ui":{"label":{"en-US":"Area Id(s)"},"type":"input","opts":{"types":["str","json","env"]}}},{"name":"excluded_area_ids","type":"str","value":"","ui":{"label":{"en-US":"Exclude"},"type":"input","opts":{"types":["str","json","env"]}}}],"meta":{},"color":"#FFCC66","inputLabels":["entity_id OR payload.entity_id"],"outputLabels":["Is in area","Is NOT in area"],"icon":"font-awesome/fa-question","status":{"x":950,"y":130,"wires":[{"id":"0a3ab013d693ea9c","port":0},{"id":"2a3ac4b687178493","port":0},{"id":"df7b1c4f69683e42","port":0}]}},{"id":"443d06390ee2d306","type":"function","z":"fa5adcb011d2ee93","name":"isInArea","func":"const origAreaIds = env.get('area_ids');\nconst origExcludedAreaIds = env.get('excluded_area_ids');\n\nconst areaIds = (\n    (typeof origAreaIds === 'string')\n        ? (origAreaIds.length ? [ origAreaIds ] : undefined)\n        : origAreaIds\n);\n\nconst exludedAreaIds = (\n    (typeof origExcludedAreaIds === 'string')\n        ? (origExcludedAreaIds.length ? [ origExcludedAreaIds ] : undefined)\n        : origExcludedAreaIds\n);\n\nif ((areaIds?.includes(msg.area_id) ?? true)\n        && !(exludedAreaIds?.includes(msg.area_id) ?? false)) { // NOTE: Is in area(s)\n    return [ msg, null ];\n}\n\nreturn [ null, msg ];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":130,"wires":[["0a3ab013d693ea9c"],["2a3ac4b687178493"]]},{"id":"7568fd9a8c462da8","type":"switch","z":"fa5adcb011d2ee93","name":"Already has area_id?","property":"area_id","propertyType":"msg","rules":[{"t":"nempty"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":210,"y":130,"wires":[["443d06390ee2d306"],["671c80068f2a18cd"]]},{"id":"671c80068f2a18cd","type":"subflow:63c69a20e33faa2a","z":"fa5adcb011d2ee93","name":"","x":420,"y":180,"wires":[["443d06390ee2d306"]]},{"id":"0a3ab013d693ea9c","type":"change","z":"fa5adcb011d2ee93","name":"Set status","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"fill\":\"green\",\"text\":\"Is in area - [\" & $.area & \"]\"}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":750,"y":40,"wires":[[]]},{"id":"2a3ac4b687178493","type":"change","z":"fa5adcb011d2ee93","name":"Set status","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"fill\":\"red\",\"text\":\"Is not in area(s) - [\" & $.area & \"]\"}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":750,"y":220,"wires":[[]]},{"id":"df7b1c4f69683e42","type":"status","z":"fa5adcb011d2ee93","name":"","scope":["671c80068f2a18cd","443d06390ee2d306"],"x":90,"y":310,"wires":[[]]}]

Sub-Flows

"Is in area?" Sub-Flow

The Javascript of isInArea:

const origAreaIds = env.get('area_ids');
const origExcludedAreaIds = env.get('excluded_area_ids');

const areaIds = (
  (typeof origAreaIds === 'string')
    ? (origAreaIds.length ? [ origAreaIds ] : undefined)
    : origAreaIds
);

const exludedAreaIds = (
  (typeof origExcludedAreaIds === 'string')
    ? (origExcludedAreaIds.length ? [ origExcludedAreaIds ] : undefined)
    : origExcludedAreaIds
);

if ((areaIds?.includes(msg.area_id) ?? true)
    && !(exludedAreaIds?.includes(msg.area_id) ?? false)) { // NOTE: Is in area(s)
  return [ msg, null ];
}

return [ null, msg ];
"get entity area" Sub-Flow

This has been updated and greatly simplified, see reply below

The Javascript of getEntityId:

msg.entity_id = (env.entity_id ?? msg.entity_id ?? msg.payload?.entity_id) 
return msg;
"join msgs" Sub-Flow (No longer needed)

No longer needed, see reply below

Here is the JavaScript of mergeMsgs:

let msgs = flow.get('msgs');
msgs = (msgs ? [ ...msgs, msg ] : [ msg ]);

const expectedMsgCount = env.get('expected_msg_count');

if (msgs.length < expectedMsgCount) {
  flow.set('msgs', msgs);
  node.status({
    fill: "yellow",
    shape: "ring",
    text: `${msgs.length} of ${expectedMsgCount} messages received`
  });

  return;
}

let newMsg = {};

msgs.forEach((it) => {
  newMsg = { ...newMsg, ...it };
});

flow.set('msgs', []);

node.status({
  fill: "green",
  shape: "dot",
  text: `${expectedMsgCount} messages received - ${new Date().toISOString()}`
});

return newMsg;

Example Usage

One way I use this sub-flow is for notifying me when my kids are up and about during the night. I can either know immediately that they are out of bed and can see what’s up, or I will see the notifications the next morning and ask them about it. However, I don’t want to be getting notifications about certain areas of the house, for instance, in my own room. So, I created a flow that notifies me if something occurred anywhere EXCEPT my room (and a couple other areas).

Here is my configuration for the Did it happen in an area I care about? node:

Questions?

I understand that this isn’t a deep dive into how each portion of the sub-flow works, that would require a much larger post. But I’m more than happy to answer any questions anyone may have. :slight_smile:

Posts That Helped Me

A simple way to get the area an entity is in is to use the JSONata function $areas

Use a current state node and in the output properties use and it will return the object containing area details.

$areas("light.bedroom")

image

3 Likes

Awesome! This is definitely MUCH easier. thank you!

Thanks to @Kermit, I don’t need the join msgs sub-flow anymore and can simplify get entity area to just this:

[{"id":"63c69a20e33faa2a","type":"subflow","name":"get entity area","info":"","category":"home_assistant","in":[{"x":60,"y":60,"wires":[{"id":"61f52ec3d0cb2289"}]}],"out":[{"x":730,"y":60,"wires":[{"id":"ba1fa0e577a36bec","port":0}]}],"env":[],"meta":{},"color":"#0b99da","icon":"font-awesome/fa-info","status":{"x":730,"y":180,"wires":[{"id":"2b74f012bcbcb2fb","port":0},{"id":"2d31877b7fb12dec","port":0}]}},{"id":"2b74f012bcbcb2fb","type":"change","z":"63c69a20e33faa2a","name":"Set status","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"text\":\"{{area.area_id}} ({{area.name}})\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":120,"wires":[[]]},{"id":"2d31877b7fb12dec","type":"status","z":"63c69a20e33faa2a","name":"","scope":["61f52ec3d0cb2289","ba1fa0e577a36bec"],"x":100,"y":180,"wires":[[]]},{"id":"61f52ec3d0cb2289","type":"link call","z":"63c69a20e33faa2a","name":"","links":["3d49ee15290810ba"],"linkType":"static","timeout":"30","x":190,"y":60,"wires":[["ba1fa0e577a36bec"]]},{"id":"ba1fa0e577a36bec","type":"api-current-state","z":"63c69a20e33faa2a","name":"add area to msg","server":"6a0a0747.044898","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"update.home_assistant_core_update","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"area","propertyType":"msg","value":"$areas($.entity_id)","valueType":"jsonata"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":400,"y":60,"wires":[["2b74f012bcbcb2fb"]]}]