How can I reduce duplicated JS code

I’ve written out a template for my chore data stored in the Grocy sensor, but as you can see, I have a ton of duplicated code in the JS templating portions of the custom button-card.

Normally, I would create a sensor but since the chores are stored as dictionaries within a list…within a list in an attribute for a single sensor, I’m not sure how I can break it out.

Example of how to access chore id 6, yes 6, with jinja templating:
{{ state_attr('sensor.grocy_chores', 'chores')[5] }}

Is there a way to pass a variable into a sensor, like I need the data for chore id 25? Call a script from within a JS template? A way to store this JS elsewhere and reference it? Should I write something in python and somehow reference it? Or am I stuck with this mess of a code?

I’ve tried searching around and I usually find what I’m looking for, but as you can imagine, searching for “home assistant” and “template” yields an insane amount of results either out of date or referencing some completely different templating feature…

  chore_button:
    template: base
    variables:
      chore_id: default
    show_icon: true
    show_label: true
    aspect_ratio: 3/0.5
    styles:
      icon:
        - top: 0%
        - left: '-12%'
      name:
        - font-size: 17px
        - line-height: 18px
        - top: 30%
        - left: 14.5%
      label:
        - top: 58%
        - right: 5%
      custom_fields:
        - top: 20%
    state:
      - operator: template
        value: |
          [[[
            var item = states['sensor.grocy_chores'].attributes.chores
            var list_position = variables.chore_id - 1

            var due_date = item[list_position]['next_estimated_execution_time'];

            var one_day = 24*60*60*1000;
            var today = new Date();

            today.setHours(0,0,0,0)

            var split_date = due_date.split(/[- :T]/)

            var parsed_due_date = new Date(split_date[0], split_date[1]-1, split_date[2]);
            parsed_due_date.setHours(0,0,0,0)

            var difference = (parsed_due_date - today) / one_day;
            return difference < -2
          ]]]
        styles:
          icon:
            - color: red
      - operator: template
        value: |
          [[[
            var item = states['sensor.grocy_chores'].attributes.chores
            var list_position = variables.chore_id - 1

            var due_date = item[list_position]['next_estimated_execution_time'];

            var one_day = 24*60*60*1000;
            var today = new Date();

            today.setHours(0,0,0,0)

            var split_date = due_date.split(/[- :T]/)

            var parsed_due_date = new Date(split_date[0], split_date[1]-1, split_date[2]);
            parsed_due_date.setHours(0,0,0,0)

            var difference = (parsed_due_date - today) / one_day;
            return (-2 <= difference) && (difference < 0);
          ]]]
        styles:
          icon:
            - color: orange
      - operator: template
        value: |
          [[[
            var item = states['sensor.grocy_chores'].attributes.chores
            var list_position = variables.chore_id - 1
            
            var due_date = item[list_position]['next_estimated_execution_time'];

            var one_day = 24*60*60*1000;
            var today = new Date();

            today.setHours(0,0,0,0)

            var split_date = due_date.split(/[- :T]/)

            var parsed_due_date = new Date(split_date[0], split_date[1]-1, split_date[2]);
            parsed_due_date.setHours(0,0,0,0)

            var difference = (parsed_due_date - today) / one_day;
            return difference == 0
          ]]]
        styles:
          icon:
            - color: green
      - operator: template
        value: |
          [[[
            var item = states['sensor.grocy_chores'].attributes.chores
            var list_position = variables.chore_id - 1
            
            var due_date = item[list_position]['next_estimated_execution_time'];

            var one_day = 24*60*60*1000;
            var today = new Date();

            today.setHours(0,0,0,0)

            var split_date = due_date.split(/[- :T]/)

            var parsed_due_date = new Date(split_date[0], split_date[1]-1, split_date[2]);
            parsed_due_date.setHours(0,0,0,0)

            var difference = (parsed_due_date - today) / one_day;
            return difference > 0
          ]]]
        styles:
          card:
            - color: 'rgba(255, 255, 255, 0.3)'
            - background-color: 'rgba(215, 215, 215, 0.3)'
          icon:
            - color: 'rgba(255, 255, 255, 0.3)'
    name: |
      [[[ 
        var item = states['sensor.grocy_chores'].attributes.chores
        var list_position = variables.chore_id - 1
        
        return item[list_position]['name'];
      ]]]
    label: |
      [[[
        var item = states['sensor.grocy_chores'].attributes.chores
        var list_position = variables.chore_id - 1

        var due_date = item[list_position]['next_estimated_execution_time'];

        var one_day = 24*60*60*1000;
        var today = new Date();

        today.setHours(0,0,0,0)

        var split_date = due_date.split(/[- :T]/)

        var parsed_due_date = new Date(split_date[0], split_date[1]-1, split_date[2]);
        parsed_due_date.setHours(0,0,0,0)

        var difference = (parsed_due_date - today) / one_day;
        
        if (difference < -1) {
          return 'Due ' + Math.round((difference * -1)) + ' days ago.';
        } else if (difference == -1) {
          return 'Due yesterday.';
        } else if (difference == 0) {
          return 'Due today.';
        } else if ( difference == 1 ) {
          return 'Due tomorrow.';
        } else {
          return 'Due in ' + Math.round(difference) + ' days.'
        }
      ]]]
    tap_action:
      action: call-service
      service: grocy.execute_chore
      service_data:
        chore_id: |
          [[[ 
            var item = states['sensor.grocy_chores'].attributes.chores
            var list_position = variables.chore_id - 1
                   
            return item[list_position]['id'];
          ]]]
      confirmation:
        text: |
          [[[ return "You are marking this task as completed."; ]]]
    hold_action:
      action: call-service
      service: browser_mod.popup
      service_data:
        title: |
          [[[ 
            var item = states['sensor.grocy_chores'].attributes.chores
            var list_position = variables.chore_id - 1

          return item[list_position]['name'] ]]]      
        card:
          type: vertical-stack
          cards:
            - type: markdown
              content: |
                [[[ 
                  var list_position = variables.chore_id - 1;
                  var item = states['sensor.grocy_chores'].attributes.chores;
                  
                  var last_completed_date = item[list_position]['last_tracked_time'];
                  var split_date = last_completed_date.split(/[- :T]/);
                  var parsed_last_completed_date = split_date[0] + "-" + (split_date[1]) + "-" + split_date[2];
                  
                  var next_due_date = item[list_position]['next_estimated_execution_time'];
                  var split_date = next_due_date.split(/[- :T]/);
                  var parsed_next_due_date = split_date[0] + "-" + (split_date[1]) + "-" + split_date[2];
                  
                  var interval = item[list_position]['period_days'];
                  var description = item[list_position]['description'];
                   
                  var last_completed_date_str = '**Last completed date:** ' + parsed_last_completed_date;
                  var next_due_date_str = '\n**Next due date:** ' + parsed_last_completed_date;
                  var interval_str = '\n**Interval:** ' + interval + ' days';
                  var description_str = '\n\n' + description;
                  
                  return  last_completed_date_str + interval_str + next_due_date_str + description_str;
                ]]]

Output:
image

Edit to add my changes:

I created a sensor using petro’s template:

      chores:
        friendly_name: Chores
        value_template: > 
            {{ state_attr('sensor.grocy_chores', 'chores')|length }}
        attribute_templates:
          chores: |
            {%- set ns = namespace(output=[]) %}
            {%- for chore in state_attr('sensor.grocy_chores', 'chores') -%}
            
              {%- set last_completed_date = chore['last_tracked_time'].date() -%}
              {%- set due_date = chore['next_estimated_execution_time'].date() -%}  
              {%- set days_until_due = (due_date - now().date()).days -%}
            
              {%- if days_until_due < -2 -%}
                {%- set status = "Critical" -%}
              {%- elif -2 <= days_until_due and days_until_due < 0 -%} 
                {%- set status = "Warning" -%}
              {%- elif days_until_due == 0 -%}
                {%- set status = "To Do" -%}
              {%- else -%}  
                {%- set status = "Done" -%}
              {%- endif -%} 
            
              {%- set ns.output = ns.output + [ 
                '"{0}": {{ "name": "{1}", "last_completed_date": "{2}", "interval": "{3}", "due_date": "{4}", "days_until_due": "{5}", "status": "{6}" }}'.format(chore["id"], chore["name"], last_completed_date, chore["period_days"], due_date, days_until_due, status) 
              ] %}
            
            {%- endfor -%}
            {{ '{' ~ ns.output | join(', ') ~ '}' }} 

and then updated the button card template with:

  chore_button:
    variables:
      chore_id: default
    .....
    state:
      - operator: template
        value: |
          [[[
            var status = states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["status"];
            return status == 'Critical';
          ]]]
        styles:
          icon:
            - color: red
      - operator: template
        value: |
          [[[
            var status = states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["status"];
            return status == 'Warning';
          ]]]
        styles:
          icon:
            - color: orange
      - operator: template
        value: |
          [[[
            var status = states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["status"];
            return status == 'To Do';
          ]]]
        styles:
          icon:
            - color: green
      - operator: template
        value: |
          [[[
            var status = states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["status"];
            return status == 'Done';
          ]]]
        styles:
          card:
            - color: 'rgba(255, 255, 255, 0.3)'
            - background-color: 'rgba(215, 215, 215, 0.3)'
          icon:
            - color: 'rgba(255, 255, 255, 0.3)'
    name: |
      [[[ return states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["name"]; ]]]
    label: |
      [[[
        var days_until_due = states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["days_until_due"];
        
        if (days_until_due < -1) {
          return 'Due ' + Math.round((days_until_due * -1)) + ' days ago.';
        } else if (days_until_due == -1) {
          return 'Due yesterday.';
        } else if (days_until_due == 0) {
          return 'Due today.';
        } else if ( days_until_due == 1 ) {
          return 'Due tomorrow.';
        } else {
          return 'Due in ' + Math.round(days_until_due) + ' days.'
        }
      ]]]
    tap_action:
      action: call-service
      service: grocy.execute_chore
      service_data:
        chore_id: |
          [[[ return variables.chore_id; ]]]
      confirmation:
        text: |
          [[[ return "You are marking this task as completed."; ]]]
    hold_action:
      action: call-service
      service: browser_mod.popup
      service_data:
        title: |
          [[[ return states['sensor.chores'].attributes.chores[variables.chore_id.toString()]["name"]; ]]]    
        card:
          type: vertical-stack
          cards:
            - type: markdown
              content: |
                [[[ 
                  var chore = states['sensor.chores'].attributes.chores[variables.chore_id.toString()];

                  var last_completed_date_str = '**Last completed date:** ' + chore["last_completed_date"];                  
                  var interval_str = '\n**Interval:** ' + chore["interval"] + ' days';
                  var next_due_date_str = '\n**Next due date:** ' + chore["due_date"] + "\n";
                  var description = states['sensor.grocy_chores'].attributes.chores[variables.chore_id - 1]["description"]
                  
                  return  last_completed_date_str + interval_str + next_due_date_str + description;
                ]]]

I use the status variable to control what gets displayed, so the “command center” tablet looks like:
image

And we get a daily report sent via slack that gives us the same list.

Everything else is used to handle the buttons and user experience.

No, it’s not possible. However, what you could do is create a template sensor that does all those calculations so the frontend isn’t doing them. Then just access the data in the js.

EDIT: Looking at your code, it doesn’t seem like it would even be worth it doing a change like that. I only see about 10 lines of regurgitated code.

So, how are you defining these in your view? Are you hard coding the chore_id for the button variable?

From my understanding, I can’t do the calculations in a sensor because the calculations are by id.

Either I’d have to pass the id into a sensor (which I’m pretty sure I can’t do) or I guess theoretically, I could calculate all of them in a sensor and return them, but with how many ids I have, that would surely put me over the character limit for a sensor output.

This is just a button template, so when I call the button later, it looks like this:

- type: 'custom:button-card'
  template: chore_button
  icon: 'mdi:watering-can'
  variables:
    chore_id: 4

Yeah ok, so you can do a few different things:

  1. Create a calculation sensor that just aggregates the data. Basically, it’ll be identical to the chores attribute, but your calculations will already be… calculated.

  2. Create sensors for each chore_id with the calculations.

here’s an example of #1

sensor:
- platform: template
  sensors:
    lovelace_chores:
      value_template: "{{ state_attr('sensor.grocy_chores', 'chores') | length }}"
      attribute_templates:
        chores: >
          {% set ns = namespace(out=[]) %}
          {% for chore in state_attr('sensor.grocy_chores', 'chores') %}
          {% set difference = .... %}
          {% set due = .... %}
          {% set ns.out = ns.out + [ (difference, due) ] %}
          {% endfor %}
          {{ ns.out }}

It’ll create a list attribute that’s formatted like this:

[
(difference, due),
(difference, due),
(difference, due),
(difference, due),
(difference, due),
(difference, due),
]

Then to access it:

states['sensor.lovelace_chores'].attributes.chores[0] # Difference
states['sensor.lovelace_chores'].attributes.chores[1] # Due

That was similar to what I was thinking, but the sensor character limit would come into play, correct? I have 60 chores so each chore could only have 4 characters.

Only states are limited to 255 characters. State attributes have no limit

…interesting.

I’ll move forward with this and report back. Thanks for the insight!

1 Like

If you’re feeling up to it, you can make dictionaries and access the information that way. But you have to piece together a string and let the template resolver turn it into a dictionary.

I was actually looking into creating a dictionary last night, but I couldn’t figure out how to accomplish it in jinja neatly.

I ended up with something similar to this:

{
  {%- for chore in state_attr('sensor.grocy_chores', 'chores') -%}

    {%- set due_date = chore['next_estimated_execution_time'].date() -%}
    {%- set difference = now().date() - due_date -%}
    
    {{ chore["id"] }}: ("{{ due_date }}", {{ difference }}),

  {%- endfor -%}
}

Is enclosing it in braces what you mean by letting the template resolver handle it? Or is there something else I’m unaware of?

So, output a string that looks like a dictionary and the template resolver will turn it into a dictionary.

{%- set x = 1 %}
{%- set y = 2 %}
{{ "{{ \"label\": \"{0}\", \"value\": {1}}}".format(x, y) }}

EDIT:

to escape curly brackets in a string, use it twice. '{{' will return a single '{' as a string

Aha, man you’re just solving all sorts of annoyances I was having. I couldn’t get it to cast as a dict when I referenced my sensor, this is great!

Okay, I’ve tried using your format, but I’m assuming it doesn’t work in a for-loop?

{%- for chore in state_attr('sensor.grocy_chores', 'chores') -%}

  {%- set due_date = chore['next_estimated_execution_time'].date() -%}
  {%- set difference = (now().date() - due_date).days -%}

  {{ "{{ \"{0}\": {1} }}".format(chore["id"], due_date) }}

{%- endfor -%}

It does, but you need to make a useable object. For that, you’d need a list.

{%- set ns = namespace(output=[]) %}
{%- for chore in state_attr('sensor.grocy_chores', 'chores') -%}

  {%- set due_date = chore['next_estimated_execution_time'].date() -%}
  {%- set difference = (now().date() - due_date).days -%}

  {%- set ns.output = ns.output + [ '{{ "{0}": {1} }}'.format(chore["id"], due_date) ] %}

{%- endfor -%}
{{ '[' ~ ns.output | join(', ') ~ ']' }}

That’s similar to where I ended up except I resolved to just have a list of lists:

{% set ns = namespace(chores=[]) %}

{%- for chore in state_attr("sensor.grocy_chores", "chores") -%}

  {%- set last_completed_date = chore['last_tracked_time'].date() -%}
  {%- set due_date = chore['next_estimated_execution_time'].date() -%}
  {%- set days_until_due = (due_date - now().date()).days -%}

  {% set ns.chores = ns.chores + [ (
        chore["id"], chore["name"], days_until_due, last_completed_date|string, due_date|string, chore["period_days"], chore["description"]
     ) ] 
   %}

{% endfor %}

{{ ns.chores }}

Your code still outputs a string:

But through trying to create my own, I learned that if I try to pass in a raw date, it will convert the whole object to a string so you can see in my version where I’m casting the date values to a string in the creation step.

I tried that same hack with yours and it didn’t work but I used a different value in place of the date and it converted to a list as expected. I’m unsure of what’s going on, so I’m assuming some weird jinja thing.


versus

After implementing the changes, I was able to reduce the button card template from 250 lines to 130 lines and given that the logic is now handled in one place, I’d say that was worth the effort. If I could create a true dictionary, that would be even better. For now I’m satisfied.

Thanks for your help!

Ah, if you wrap the date in quotes in the template I provided, it would be a list of dictionaries. But if you want a dictionary as the main object, try this:

{%- set ns = namespace(output=[]) %}
{%- for chore in state_attr('sensor.grocy_chores', 'chores') -%}

  {%- set due_date = chore['next_estimated_execution_time'].date() -%}
  {%- set difference = (now().date() - due_date).days -%}

  {%- set ns.output = ns.output + [ '"{0}": {{ "difference": {1}, "date": "{2}" }}'.format(chore["id"], difference, due_date) ] %}

{%- endfor -%}
{{ '{' ~ ns.output | join(', ') ~ '}' }}
1 Like

I updated my original post with the changes I made. Your help was a total game changer!

1 Like