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:
- Ability to provide a string or list of strings specifying which area(s) I expected the entity to belong (IE, an inclusion list).
- Ability to provide a string or list of strings specifying which area(s) I expected the entity to NOT belong (IE, an exclusion list).
- 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.