Expiration Date Monitor with notification

Originally posted hereopen in new window 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 Listopen in new window 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/open in new window

# 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

image|498x346

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.

image|690x93

[{"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":[[]]}]

image|551x297

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.