JSON Object of all input_texts

Hi all. I’m attempting to create an expiration date monitor using Node Red. I have a list of input_text that looks like this:

greenbeans:
  name: Green Beans
  initial: 03/07/2020

corn:
  name: Canned Corn
  initial: 09/17/2021

rice:
  name: 10lb Rice
  initial: 15/13/2025

Don’t mind the place-holder dates. They’re for testing purposes only.

I’d like to use Node Red to loop through the dates and compare them to today, then notify me if something will expire within 90 days.

Step one is to get the correct input_texts into a function node so I can parse the date data. I can take the current state and compare it to today using JSON and report if the dates match, but I’d need to do that with all input_texts that have to do with food in my pantry. Are input_texts the way to go for this? I’d prefer a solution that can dynamically update an array or something when I add things to my pantry, or take them away.

I’m thinking there’s a way to do this with templates, but I don’t know the best way to go about that. I also considered looking into the REST api to grab all input_texts, but not sure where to start on that route either. Can anyone point me in the right direction to get an array of all input_texts?

Thanks!

I’ve solved this. I created a template sensor and messed around with the jinja2 web docu to create a sensor that’s formatted nicely so I can run it through a JS node. Here’s the template sensor:

    stock_expiration_dates:
      value_template: >
        {%- for input_text in states.input_text -%}
        {{ input_text.entity_id ~ "=" ~ input_text.state ~ ", " }}
        {%- endfor -%}

and output

input_text.corn=09/17/2021, input_text.greenbeans=03/07/2020, input_text.rice=15/13/2025,

The obvious problem in using this template, is that it’s going to grab ALL input_texts. I’m not currently using input texts for anything other than this (for now) but I suspect this will need to be changed if I use input_text for anything else.

The template solution would be something along the lines of:

{% set domain = 'input_text' %}
{% set exp_dates = states[domain] | map(attribute='state') | list %}
{{ exp_dates }}

Try it in the template editior.

EDIT: too slow.

Thanks Tom! Your solution gives me ideas for other projects, so it’s helpful!

1 Like

Petro wrote a great post on selecting multiple states and attributes here:

1 Like

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

image

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

image

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

2 Likes

Wow! That’s a much more elegant solution, and I’m definitely going to adopt that today. I had no idea about the shopping list, and that’s absolutely perfect and what I was looking for.

I ended up just creating a single function node and coding it all within that. I even put my items in an array there, because the input_texts were bugging out if I had more than (4) entries. Three entries and it worked find, but when I added the fourth one, the sensor status turns to “Unknown”.

The way you have it is much more modular, and much more robust. Thank you very much for taking the time to prototype this out. I’ll definitely be setting up something similar today.

I deleted my single function node and was able to work your solution in. I have to say, thank you so much for providing the node codes. Some of that is foreign to me, and I’ve really enjoyed looking up the functions you used and learning new ways of doing things. I’m super excited to have this working!

I have some other time comparisons going on of my own design, and I can’t believe how simple your moment setup is. Definitely inspires me to learn more about that and change that over. I may try my hand at what you suggested, input validation etc. Would be a good project.

Hi. This is a very cool decision. Can you please help me? Is it possible to record the date in the “shopping list” not MM.DD.YY and in the format DD.MM.YY. It would be more familiar to me, but I can’t figure out how to do it myself