Expiration Date Monitor with notification
Originally posted here on the Home Assistant Forums.
Here's a demo showing how to build a system to track the expiration date of items in the pantry. Using the Home Assistant Shopping List as a way to input and update a list of items and their expiration date.
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 Window
initial: 90
min: 30
max: 120
step: 1
unit_of_measurement: days
icon: mdi:calendar-clock
Simple Lovelace config
type: vertical-stack
cards:
- title: Pantry Items
type: shopping-list
- type: entities
entities:
- input_number.pantry_expiration
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":"e78bbe2c.9141","type":"inject","z":"56b1c979.b2c618","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":284,"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\n // check for valid date\n const expiredDate = Date.parse(exp);\n if (isNaN(expiredDate) || expiredDate > expireWindow) return;\n \n // Add item to expired list\n expItems.push({ \n name, \n exp, \n inThePast: expiredDate < Date.now()\n });\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":822,"y":1056,"wires":[["4270d967.43cc08"]]},{"id":"720213f9.9bb8fc","type":"api-current-state","z":"56b1c979.b2c618","name":"Get expiration window","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":468,"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":290,"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":674,"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":418,"y":1120,"wires":[["c96d522b.7cfbb"]]},{"id":"c96d522b.7cfbb","type":"function","z":"56b1c979.b2c618","name":"format","func":"const d = msg.payload;\nmsg.payload = `${d.name} expire${d.inThePast ? 'd' : 's'} ${d.pretty}`;\nreturn msg;","outputs":1,"noerr":0,"x":544,"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":882,"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.