REST - Nutrislice JSON Values

Hey I just drove through Bettendorf today, small world. You probably have already figured it out by now, but here is the fix you were looking for just in case:

Change:

{{  day.menu_items | selectattr("food.food_category","equalto","entree") | slice(2) | first | join('; ', attribute="food.name") }}<br/>

To:

 {{  day.menu_items | selectattr("food.name", 'defined') | slice(1) | first | join('; ', attribute="food.name") }}<br/>

The defined part is important because it will error if the value doesn’t exist. Also changing slice(2) to slice(1) gives all the results. Otherwise it was cutting off some menu items for me. I also went with food.name because at least for our school the food category is never used. I took your code and built on it. Here is what I am using:

List meals for entire week:

{% for day in state_attr('sensor.school_lunch', 'days') |
selectattr("menu_items") | selectattr("date", "greaterthan",
(now()-timedelta(hours=12)).strftime("%Y-%m-%d")) %}

{%- if day.date == now().strftime("%Y-%m-%d") -%}<b><u>TODAY</b></u>:{{" "}}

{%- elif day.date == (now()+timedelta(hours=24)).strftime("%Y-%m-%d")
-%}<b><u>TOMORROW</b></u>:{{" "}}{{"
"}}

{%- else -%}<b><u> {{- strptime(day.date, "%Y-%m-%d").strftime("%A").upper() -}}</b></u>:{{"
"}}

{%- endif -%}

{% for result in day.menu_items | selectattr("position", 'defined')  %}

{%- if result.text != "" -%}
<br><b><i>{{ result.text }}</b></i><br>
{%- endif -%}

{%- if result.food is not none -%}
{{ result.food.name }}<br>
{%- endif -%}

{% endfor %}

<br>

{% endfor %}

This method lists each item in order so that I could include the section titles as well (ex: alternate). It ends up looking like this:

Screenshot_9-9-2024_121926_localha.czerkacorp.com

I also created a grid card to show the lunch menu with pictures for the next two days. I have the screen real estate so mine is larger than some people may want. I set it to show today and tomorrow if it is before 8AM or to show the next two days if it is after 8AM. I also hide the whole thing on Saturdays:

square: false
type: grid
columns: 2
cards:
  - type: markdown
    content: |-
      <center>
      {% for day in state_attr('sensor.school_lunch', 'days') |
      selectattr("menu_items") | selectattr("date", "equalto",
      now().strftime("%Y-%m-%d")) %}

      <b><u>TODAY</b></u>


      {% for result in day.menu_items | selectattr("position", 'defined')  %}

      {%- if result.text != "" -%}
      <font color='yellow'><br><b><i>{{ result.text }}</b></i><br><br></font>
      {%- endif -%}

      {%- if result.food is not none -%}
      <img src='{{ result.food.image_url }}' width='150' ><br>
      {%- endif -%}

      {%- if result.food is not none -%}
      {{ result.food.name }}<br><br>
      {%- endif -%}

      {% endfor %}

      <br>

      {% endfor %}
      </center>
    visibility:
      - condition: state
        entity: binary_sensor.nutrislice_before_8
        state: 'on'
  - type: markdown
    content: |-
      <center>
      {% for day in state_attr('sensor.school_lunch', 'days') |
      selectattr("menu_items") | selectattr("date", "equalto",
      (now()+timedelta(hours=24)).strftime("%Y-%m-%d")) %}

      <b><u>TOMORROW</b></u>


      {% for result in day.menu_items | selectattr("position", 'defined')  %}

      {%- if result.text != "" -%}
      <font color='yellow'><br><b><i>{{ result.text }}</b></i><br><br></font>
      {%- endif -%}

      {%- if result.food is not none -%}
      <img src='{{ result.food.image_url }}' width='150' ><br>
      {%- endif -%}

      {%- if result.food is not none -%}
      {{ result.food.name }}<br><br>
      {%- endif -%}

      {% endfor %}

      <br>

      {% endfor %}
      </center>
  - type: markdown
    content: >+
      <center>

      {% for day in state_attr('sensor.school_lunch', 'days') |

      selectattr("menu_items") | selectattr("date", "equalto",

      (now()+timedelta(hours=48)).strftime("%Y-%m-%d")) %}


      <b><u> {{- strptime(day.date, "%Y-%m-%d").strftime("%A").upper()
      -}}</b></u>



      {% for result in day.menu_items | selectattr("position", 'defined')  %}


      {%- if result.text != "" -%}

      <font color='yellow'><br><b><i>{{ result.text }}</b></i><br><br></font>

      {%- endif -%}


      {%- if result.food is not none -%}

      <img src='{{ result.food.image_url }}' width='150' ><br>

      {%- endif -%}


      {%- if result.food is not none -%}

      {{ result.food.name }}<br><br>

      {%- endif -%}


      {% endfor %}


      <br>


      {% endfor %}

      </center>

    visibility:
      - condition: state
        entity: binary_sensor.nutrislice_before_8
        state: 'off'
title: School Lunch
visibility:
  - condition: state
    entity: sensor.day_of_the_week
    state_not: '6'

I’m no pro so there may be a better way of doing this, but it worked for me. Thanks for sharing your part!

1 Like

@AdmiralAckbar This is really helpful, thank you for sharing your work!

As someone who is just getting my fingers into this kind of thing I wonder if I could ask:

How would you change the following code (just copied/pasted and changed the sensor) so that it only returns items where "food_category" = "entree" and then ignoring the first two items because they are for breakfast.

type: markdown
content: >-
{% for day in state_attr('sensor.bcps_menu', 'days') | selectattr("menu_items") | selectattr("date", "greaterthan", (now()-timedelta(hours=12)).strftime("%Y-%m-%d")) %}

{%- if day.date == now().strftime("%Y-%m-%d") -%}<b><u>TODAY</b></u>:{{" "}}

{%- elif day.date == (now()+timedelta(hours=24)).strftime("%Y-%m-%d") -%}<b><u>TOMORROW</b></u>:{{" "}}{{" "}}

{%- else -%}<b><u> {{- strptime(day.date, "%Y-%m-%d").strftime("%A").upper() -}}</b></u>:{{" "}}

{%- endif -%}

{% for result in day.menu_items | selectattr("position", 'defined')  %}

{%- if result.text != "" -%}

<br><b><i>{{ result.text }}</b></i><br>

{%- endif -%}

{%- if result.food is not none -%}

{{ result.food.name }}<br>

{%- endif -%}

{% endfor %}

<br>

{% endfor %}

I’ve tried splicing and joining and adding ifs and stuff but haven’t gotten anything to output yet.

1 Like

funny you ask this because i was wondering something very similar (excluding breakfast), and also for the same district! following

1 Like

I think I’ve figured out that just adding a selectattr("food_category", 'equalto', "entree") to the for loop won’t work because food_category is further down the hierarchy under the ā€œfoodā€ record.

So ideally I think it would be: select attribute: days, greater than today | select attribute, menu_items | select attribute, food | select attribute, food_category, equal to entree. And then start the for the loop at item 3.

Just need to figure out how to convert that to working code.

Edit: Used ChatGPT to get to this point:

{% set filtered_days = state_attr('sensor.bcps_menu', 'days')
  | selectattr('date', '>', (now() - timedelta(hours=12)).strftime('%Y-%m-%d'))
%}

{% for day in filtered_days %}
    {%- if day.date == now().strftime("%Y-%m-%d") -%}
    <b><u>TODAY</b></u>:{{" "}}

    {%- elif day.date == (now() + timedelta(days=1)).strftime("%Y-%m-%d") -%}
    <b><u>TOMORROW</b></u>:{{" "}}{{"
    "}}

    {%- else -%}
    <b><u>{{- strptime(day.date, "%Y-%m-%d").strftime("%A").upper() -}}</b></u>:{{"
    "}}

    {%- endif -%}

    {% for result in day.menu_items 
      | selectattr('food', 'defined') 
      | selectattr('food', '!=', none)
      | selectattr('food.food_category', 'equalto', 'entree') %}
        
        {%- if result.text != "" -%}
        <br><b><i>{{ result.text }}</b></i><br>
        {%- endif -%}

        {%- if result.food is not none -%}
        {{ result.food.name }}<br>
        {%- endif -%}
    {% endfor %}

<br>
{% endfor %}

Which outputs:

TODAY: 
Assorted Savory Bread
Mini Waffles
Chicken Nuggets
Yogurt Entree, 8oz
Bento Box with Turkey & Cheese Pinwheels
Bento Box with Cheese


TOMORROW:
Assorted Muffin
Assorted Savory Bread
Cereal, Assorted
Pancake Wrapped Turkey Sausage
Hot Dog
Yogurt Entree, 8oz
Chef Salad
Bento Box with Tuna


FRIDAY:
Granola Round
Manager's Choice EntrƩe
Personal Pizza (Round)
Yogurt Entree, 8oz
Turkey & Ham Club Wrap
Mgr Choice Vegetarian Salad/Bento

Now trying to figure out how to skip the first two items, and maybe only keep items in position 1 and 2.

1 Like

Hopefully, I’m not spamming this. I’ve got this working more or less how I want it, though.

type: markdown
content: >-
  {% set filtered_days = state_attr('sensor.bcps_menu', 'days') |
  selectattr('date', '>', (now() - timedelta(hours=12)).strftime('%Y-%m-%d')) %}

  {% for day in filtered_days %}
  {% set day_of_week = strptime(day.date, "%Y-%m-%d").strftime("%A") %}

  {# Skip weekends (Saturday and Sunday) #}
  {% if day_of_week in ['Saturday', 'Sunday'] %}
    {% continue %}
  {% endif %}

  {%- if day.date == now().strftime("%Y-%m-%d") -%}
  <b><u>TODAY</b></u>:<br>{{" "}}

  {%- elif day.date == (now() + timedelta(days=1)).strftime("%Y-%m-%d") -%}
  <b><u>TOMORROW</b></u>:<br>{{" "}}{{"
  "}}

  {%- else -%}
  <b><u>{{- day_of_week.upper() -}}</b></u>:{{"
  "}}

  {%- endif -%}

  {# Counter for tracking the number of position 1 items #}
  {% set position_1_counter = 0 %}

  {% for result in day.menu_items 
    | selectattr('food', 'defined') 
    | selectattr('food', '!=', none)
    | selectattr('food.food_category', 'equalto', 'entree')
    | selectattr('position', 'in', [1, 2, 3, 4]) %}

    {%- if result.position == 1 %}
      {% set position_1_counter = position_1_counter + 1 %}
      {%- if position_1_counter == 2 %}
  
      {% endif %}
    {% endif %}

    {%- if result.text != "" -%}
    <b><i>{{ result.text }}</b></i>
    {%- endif -%}

    {%- if result.food is not none -%}
    {{ result.food.name }}
    {%- endif %}
  {% endfor %}

  {% endfor %}

Which returns:
image
I’d like to have a line, or an icon or something where it has a break for the second ā€˜position = 1’ but this code has that line break, but nothing I put in that space shows up in the card…not sure why.

I cannot test mine against a category because our district doesn’t use it, but try something like this to filter out the first two and entree only:

{% set ns = namespace(count=0) %}

{% for day in state_attr('sensor.bcps_menu', 'days') |
selectattr("menu_items") | selectattr("date", "greaterthan",
(now()-timedelta(hours=12)).strftime("%Y-%m-%d")) %}


{%- if day.date == now().strftime("%Y-%m-%d") -%}<b><u>TODAY</b></u>:{{" "}}

{%- elif day.date == (now()+timedelta(hours=24)).strftime("%Y-%m-%d")
-%}<b><u>TOMORROW</b></u>:{{" "}}{{"
"}}

{%- else -%}<b><u> {{- strptime(day.date, "%Y-%m-%d").strftime("%A").upper() -}}</b></u>:{{"
"}}

{%- endif -%}

  {% for result in day.menu_items | selectattr("position", 'defined')  %}

  {%- if result.text != "" -%}
  <font color='yellow'><br><b><i>{{ result.text }}</b></i><br><br></font>
  {%- endif -%}

  {%- if result.food is not none -%}
    {%- if 'Entree' in result.food.food_category -%}    
    {% set ns.count = ns.count + 1 %}
    {%- if ns.count > 2 -%}
      {{ result.food.name }}<br>
    {%- endif -%}
  {%- endif -%}
  
  {%- endif -%}

  {% endfor %}

  <br>

  {% endfor %}

my (our?) district occasionally has days with 3 breakfast entrees, though i think its fairly rare

To add to this. I’m only interested in tracking tomorrow’s meal, this works:

resource_template: https://<district>.api.nutrislice.com/menu/api/digest/school/<school-name>/menu-type/lunch/date/{{(now().date() + timedelta(days=1)).strftime('%Y/%m/%d')}}?format=json

Great work, BTW… this was really helpful. I didn’t use a template, I’m pushing the data out through SharpTools and displaying it on my tablets that way.

I have been having trouble getting the correct URL format for our district. Anyone have any ideas? The calendar works via the following address:

Nutrislice Menus

But this throws a 404…

https://linnmark12ia.api.nutrislice.com/menu/api/digest/school/boulder-peak/menu-type/lunch/date/2025-02-01?format=json

I wasn’t able to edit my last post, but nevermind… I just realized that it has to be / instead of - for the date separator. This works perfectly:

https://linnmark12ia.api.nutrislice.com/menu/api/weeks/school/boulder-peak/menu-type/lunch/2025/02/01?format=json

Phoning a friend - Our school district switched from NutriSlice to LinqConnect, and I have been unsuccessful in adapting the code to work.

The api can be found below, any help or a nudge in the right direction would be greatly appreciated:

https://api.linqconnect.com/api/FamilyMenu?buildingId=d16a91cc-d9f5-ee11-f5528156-47f3-ee11-a85d-ed4bcd1072b1&districtId=019aa8c9-09f1-ee11-a85e-985e1ae32d4b&startDate=2-16-2025

Adding to this again as I’ve been playing around with it. I’m now passing the menu into generative AI to put a fun twist on it. The menu, as read by a randomly selected, Optimus Prime, Donald Trump, Gordon Ramsay, or a pirate.

- trigger:
  - trigger: state
    entity_id: 
        - sensor.lunch_tomorrow
        - input_button.change_munu
  action:
    - action: google_generative_ai_conversation.generate_content
      response_variable: response
      data:
        prompt: >-
          The following is a list of menu items for lunch tomorrow 
          {{ state_attr('sensor.lunch_tomorrow', 'menu_items') }}.   
          You are {{ ["Donald Trump", "Optimus Prime", "Gordon Ramsay", "a pirate"] | random }}.
          Tell me the menu in a funny way.
          Don't use more than 200 words.
          Format it to look nice on a Home Assistant Markdown Card
  sensor:
    - name: Funny Lunch Text
      unique_id: funnyllunchtext
      state: None
      attributes:
        funny_text: "{{ response.text }}"

1 Like