Your further comment prompted me into thinking of various solutions to this.
Home Assistant is, in effect, a state-machine which keeps track of entity states, as well as the change-events that give rise to state value updates. This means that, at any point in time, we can easily trigger actions based on state change events, and make enquiries on the state of any entity.
By contrast, Node-RED, as a programming language, has been designed as an event-based (visual) tool for the IOT world. To make effective use of Node-RED requires flows (more correctly, sequences) to be triggered by a well-defined event, and to then manipulate data to an end goal or purpose. Node-RED has no ‘state’ as such, with the only ‘state’ values being anything held in in-flight messages.
In your case you have two entities of interest
- people to be notified
- bins to be put out (I am from the UK, so for us they are ‘bins’ containing ‘rubbish’ and we ‘put them out’ for ‘collection’ the next day )
You have three events of interest
- at 10:00 every day
- check to see if there are bins to put out tomorrow
- if so, notify anyone who is currently at home
- anytime anyone comes home
- who has not yet been notified, notify them of the bins
- at 19:00 every day
- notify anyone who has not yet been notified
The first event is easy - you have all the states available (bins, people). The next event can be triggered by someone coming home (from not being home). The last event is more challenging as you need to know, in effect, who has been notified already.
You are solving your problem by keeping an in-flight message as ‘pending notification for someone who was not home at 10:00, until they either arrive home or it is 19:00’. As you have already noted, keeping messages ‘in-flight’ is not necessarily a good idea, and it is better for any Node-RED flow to go from trigger event to end of sequence quickly and tidily. This also helps with memory use
recovery in the underlying JavaScript V8 engine.
So, how to solve?
One way is to work off the trigger events and build a full state model each time from scratch. For the second and third events (someone comes home during the day, it is 19:00) this requires calling for a complete person state history for the past 10 hours and looking to answer the question…
“How many times has this person been home between 10:00 and 19:00?”
- if they were home at 10:00, then they were notified then
- if they came home between 10:00 and 19:00, they were notified then
- if they have not been home between 10:00 and 19:00, they need notifying now
At any point, when someone comes home, and also at 19:00, this entire ‘person-home-history’ model has to be built from scratch, which can be done but is rather complicated.
Alternatively we can save a partial ‘state-model’ to memory (or file) and work from that. I guess most would start by trying to write a timer or future trigger to Node-RED context, but in thinking about this I decided to work on a ‘notification-state’ model.
- At 10:00
- build a model of the bins to notify (if any) and the people to notify (at some point in the day)
- for anyone in the model who is home, notify them immediately
- At event ‘person comes home’
- if they have not yet been notified, notify them
- At 19:00
- for anyone in the model who has not yet been notified, notify them
The model is kept in Node-RED context, and every notification has to update the notification-state.
My experimental flow looks like this.
As you can see, it is much more complicated, but that is what happens when you want to push data to store, retrieve it later, and update it.
Node-RED context is very easy to use, and normally based on ‘memory’. This remains after a flow has ended, and is not affected by deployments, but will vanish if Node-RED is restarted. It is easy enough to modify the NR setting.js file to include an additional ‘file based’ context store. This will flush the context values in memory to a holding file (every 30 seconds) so is, for the most part, independent of NR restarts and will be restored for you automatically. In my ‘experimental’ flow I am just using standard memory-based context, but you can find all the details about file-based context here.
Here is the flow - you are welcome to experiment with this.
[{"id":"5874338da96fef94","type":"server-state-changed","z":"7021f7d89ccb8403","name":"Anyone comes home","server":"","version":5,"outputs":2,"exposeAsEntityConfig":"","entityId":"person.*","entityIdType":"regex","outputInitially":false,"stateType":"str","ifState":"home","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"topic","propertyType":"msg","value":"Home Event","valueType":"str"},{"property":"person","propertyType":"msg","value":"$entity().attributes.friendly_name","valueType":"jsonata"},{"property":"time","propertyType":"msg","value":"$entities('sensor.time').state","valueType":"jsonata"}],"x":140,"y":4560,"wires":[["8d49e74a99f6eb46"],[]]},{"id":"779edfdbf6dde4a1","type":"change","z":"7021f7d89ccb8403","name":"Notify immediately","rules":[{"t":"set","p":"list","pt":"msg","to":"[binnotify.tell[notify=\"now\"]]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":4520,"wires":[["71f2926d3af012ab"]]},{"id":"8d49e74a99f6eb46","type":"change","z":"7021f7d89ccb8403","name":"Notify on coming home","rules":[{"t":"set","p":"binnotify","pt":"msg","to":"BinNotify","tot":"flow","dc":true},{"t":"set","p":"list","pt":"msg","to":"[binnotify.tell[notify=\"yes\" and person=$$.person]]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":4560,"wires":[["71f2926d3af012ab"]]},{"id":"a3863741373ee58d","type":"inject","z":"7021f7d89ccb8403","name":"10:00","props":[],"repeat":"","crontab":"00 10 * * *","once":false,"onceDelay":0.1,"topic":"","x":110,"y":4440,"wires":[["84c44f9a06d8f8b5"]]},{"id":"84c44f9a06d8f8b5","type":"api-current-state","z":"7021f7d89ccb8403","name":"Get bins and people","server":"","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"sensor.time","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"topic","propertyType":"msg","value":"Time Event","valueType":"str"},{"property":"time","propertyType":"msg","value":"","valueType":"entityState"},{"property":"bins","propertyType":"msg","value":"$entities().*[$substringAfter(entity_id,\".\") in [\"avfallsor_metal_xxx\", \"avfallsor_mixed_xxx\", \"avfallsor_paper_xxx\", \"avfallsor_plastic_xxx\"]].{\"bin\": attributes.friendly_name, \"state\": state}\t\t","valueType":"jsonata"},{"property":"people","propertyType":"msg","value":"$entities().*[$substringBefore(entity_id,\".\")=\"person\"].{\"person\": attributes.friendly_name, \"state\": state, \"entity_id\": 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":140,"y":4480,"wires":[["1101eaf5b3f87744"]]},{"id":"1101eaf5b3f87744","type":"change","z":"7021f7d89ccb8403","name":"Build notifications in context","rules":[{"t":"set","p":"binnotify","pt":"msg","to":"(\t /* get list of bins due tomorrow where state = \"1\" */\t $dobins:=bins[state=\"1\"];\t \t /* code in service call required for each person in the list */\t $data:=[\t {\"name\": \"Geoff\", \"service\": \"mobile_app_this_phone\"},\t {\"name\": \"Other\", \"service\": \"mobile_app_other_phone\"}\t ];\t\t /* get list of persons I want to notify about this */\t $tellme:=people[person in $data.name];\t \t /* when bins are due, set notify to 'now' if home or 'yes' if not home */\t /* pick up appropriate service for the person from $data */\t $tellme:= $tellme ~> |$|($p:=person;\t {\"notify\": $exists($dobins) ? state=\"home\" ? \"now\" : \"yes\",\t \"service\": $data[name=$p].service\t })|;\t \t /* save this list of people and bins to context */\t \t {\"bins_due\": $dobins.bin~>$join(\"\\n\"),\t \"tell\": $tellme,\t \"time\": time};\t\t\t\t \t)","tot":"jsonata"},{"t":"set","p":"BinNotify","pt":"flow","to":"binnotify","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":4480,"wires":[["779edfdbf6dde4a1"]]},{"id":"1bc772aff63d91f5","type":"inject","z":"7021f7d89ccb8403","name":"19:00 - tell remaining","props":[{"p":"binnotify","v":"BinNotify","vt":"flow"},{"p":"list","v":"[binnotify.tell[notify=\"yes\"]]","vt":"jsonata"},{"p":"time","v":"19:00","vt":"str"}],"repeat":"","crontab":"00 19 * * *","once":false,"onceDelay":0.1,"topic":"","x":400,"y":4600,"wires":[["71f2926d3af012ab"]]},{"id":"26b9bbeeed9bde43","type":"change","z":"7021f7d89ccb8403","name":"Set up service call","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\t \"domain\": \"notify\",\t \"service\": list.service,\t \"data\": {\t \"title\": \"The bins need to go out today\",\t \"message\": binnotify.bins_due\t }\t}\t\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":4560,"wires":[["195db8bc265cdf18"]]},{"id":"195db8bc265cdf18","type":"api-call-service","z":"7021f7d89ccb8403","name":"","server":"","version":5,"debugenabled":false,"domain":"","service":"","areaId":[],"deviceId":[],"entityId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"all","x":970,"y":4560,"wires":[["2655593cef77d7f4"]]},{"id":"71f2926d3af012ab","type":"split","z":"7021f7d89ccb8403","name":"Each Person","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"list","x":610,"y":4560,"wires":[["26b9bbeeed9bde43"]]},{"id":"0c4ee241a96bb1af","type":"change","z":"7021f7d89ccb8403","name":"Update context","rules":[{"t":"set","p":"BinNotify","pt":"flow","to":" binnotify ~> |tell|{\"notify\": person in $$.list.person ? $$.time}|","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1260,"y":4560,"wires":[[]]},{"id":"2655593cef77d7f4","type":"join","z":"7021f7d89ccb8403","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":false,"accumulate":true,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1110,"y":4560,"wires":[["0c4ee241a96bb1af"]]}]
I am using JSONata to do the work. The Current State node uses the sensor.time entity (which you need to have added using the date and time integration) and then some code to read all the ‘person.’ entities and all the bins (trash) as listed in
$entities().*[$substringAfter(entity_id,".") in ["avfallsor_metal_xxx", "avfallsor_mixed_xxx", "avfallsor_paper_xxx", "avfallsor_plastic_xxx"]].{"bin": attributes.friendly_name, "state": state}
After that, an object is built containing the bins to put out today (state = “1”) and all the people to notify, as listed in the code
(
/* get list of bins due tomorrow where state = "1" */
$dobins:=bins[state="1"];
/* code in service call required for each person in the list */
$data:=[
{"name": "Geoff", "service": "mobile_app_this_phone"},
{"name": "Other", "service": "mobile_app_other_phone"}
];
/* get list of persons I want to notify about this */
$tellme:=people[person in $data.name];
/* when bins are due, set notify to 'now' if home or 'yes' if not home */
/* pick up appropriate service for the person from $data */
$tellme:= $tellme ~> |$|($p:=person;
{"notify": $exists($dobins) ? state="home" ? "now" : "yes",
"service": $data[name=$p].service
})|;
/* save this list of people and bins to context */
{"bins_due": $dobins.bin~>$join("\n"),
"tell": $tellme,
"time": time};
)
You will need to modify the name, as well as the service call. In my case, I have for domain notify
service calls by individual phone, so I had to add the specific service call to each person in the notification list.
The rest of the flow is straightforward. At each event, context is read back, an array (could be more than one) of people to be notified is pulled, and for each person the notification service call set up, run, and then at the end for each person notified, the notification object is updated with the time “hh:mm”. This means that future triggers can now pick out only those individuals who have not yet been notified.
I am using the new feature of splitting the array on msg.list (not msg.payload) so this will only work from Node-RED v4+ and therefore NR addon v18+
An interesting exercise, and clearly using the Wait node, even with the problem of restart/deploy is actually much easier to write.