Assuming you’re not using the Shopping List that comes with HA here’s a demo how one could use it to perform the task.
The format of the name in the shopping list would be [item] : [expiration date]
with a colon separating the item name from the expiration date.
Home Assistant stuff
First, you would have to activate the shopping list in the config https://www.home-assistant.io/integrations/shopping_list/
# Example configuration.yaml entry
shopping_list:
There’s already a lovelace card for the shopping list. I also added an input_number
to dynamically control the expiration window to check.
# Example configuration.yaml entry
input_number:
pantry_expiration:
name: Pantry Expiration Days
initial: 90
min: 30
max: 120
step: 1
Simple Lovelace config
type: vertical-stack
cards:
- title: Pantry Expiration
type: shopping-list
- type: entities
entities:
- input_number.pantry_expiration
Now for the Node-RED stuff
You can set the inject node to fire at a set time each day or every other day whatever fits your needs.
[{"id":"e58bbe2c.9141","type":"inject","z":"56b1c979.b2c618","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":300,"y":1056,"wires":[["720213f9.9bb8fc"]]},{"id":"adeab5ee.bc4098","type":"ha-api","z":"56b1c979.b2c618","name":"Get Items","debugenabled":false,"protocol":"websocket","method":"get","path":"","data":"{\"type\": \"shopping_list/items\"}","dataType":"json","location":"payload","locationType":"msg","responseType":"json","x":652,"y":1056,"wires":[["236e3cd6.fab7d4"]]},{"id":"236e3cd6.fab7d4","type":"function","z":"56b1c979.b2c618","name":"do the stuff","func":"const items = msg.payload;\n\nif (items.length === 0) return;\n\nconst expItems = [];\n\n// Current timestamp + expiration days in milliseconds\nconst expireWindow = Date.now() + msg.expDays * 8.64e7;\n\nitems.forEach(i => {\n // If the name doesn't contain the split character don't process\n // If complete set to true in the shopping list don't process\n if (!i.name.includes(\":\") || i.complete === true) return;\n\n // Split the name and remove white spaces\n const [name, exp] = i.name.split(\":\").map(x => x.trim());\n const data = { name, exp };\n\n // check for valid date\n const expiredDate = Date.parse(exp);\n if (isNaN(expiredDate) || expiredDate > expireWindow) return;\n // in the past\n if (expiredDate < Date.now()) {\n data.inThePast = true;\n }\n // Add item to expired list\n expItems.push(data);\n});\n\n// If array is empty nothing to report\nif (expItems.length === 0) return;\n\nmsg.payload = expItems;\n\nreturn msg;\n","outputs":1,"noerr":0,"x":838,"y":1056,"wires":[["4270d967.43cc08"]]},{"id":"720213f9.9bb8fc","type":"api-current-state","z":"56b1c979.b2c618","name":"Get expiration days","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"input_number.pantry_expiration","state_type":"num","state_location":"expDays","override_payload":"msg","entity_location":"","override_data":"none","blockInputOverrides":false,"x":474,"y":1056,"wires":[["adeab5ee.bc4098"]]},{"id":"4270d967.43cc08","type":"split","z":"56b1c979.b2c618","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":306,"y":1120,"wires":[["e1f6793d.9be5f8"]]},{"id":"3d3871cd.cb442e","type":"join","z":"56b1c979.b2c618","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":690,"y":1120,"wires":[["ddffd5ea.ebd528"]]},{"id":"e1f6793d.9be5f8","type":"moment","z":"56b1c979.b2c618","name":"pretty","topic":"","input":"payload.exp","inputType":"msg","inTz":"America/Los_Angeles","adjAmount":0,"adjType":"days","adjDir":"add","format":"timeAgo","locale":"en_US","output":"payload.pretty","outputType":"msg","outTz":"America/Los_Angeles","x":434,"y":1120,"wires":[["c96d522b.7cfbb"]]},{"id":"c96d522b.7cfbb","type":"function","z":"56b1c979.b2c618","name":"format","func":"msg.payload = `${msg.payload.name} ${msg.payload.inThePast ? 'expired' : 'expires'} ${msg.payload.pretty}`;\nreturn msg;","outputs":1,"noerr":0,"x":560,"y":1120,"wires":[["3d3871cd.cb442e"]]},{"id":"ddffd5ea.ebd528","type":"api-call-service","z":"56b1c979.b2c618","name":"","version":1,"debugenabled":false,"service_domain":"notify","service":"mobile_app_phone","entityId":"","data":"{\t \"title\": \"Pantry Items Expiring\",\t \"message\": $join(payload, \"\\n\")\t}","dataType":"jsonata","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":898,"y":1120,"wires":[[]]}]
There’s a lot more polish that could go into this such as being notified if the date entered in the shopping list is invalid or doesn’t have a date at all. Sort the expired list so that the closest to expiring is at the top.
To answer your original question you can use a get entities
node in NR to get a list of input_texts
. The simple way would be a Regex check against the entity ids. https://zachowj.github.io/node-red-contrib-home-assistant-websocket/cookbook/get-entities.html