Chore tracking in Home Assistant

Okay everyone, I know there are several chore tracking options around, including Grocy. The unfortunate thing about all these, is none of them fit me exact use case, and so I am trying to develop my own by combining pieces from several, but I am running into some hardships. Hopefully some of those around here that have more knowledge than me can help me with this,

Goals - Overall - Have chore tracking that is native inside home assistant using what already exists. ( this is why grocy is not an option for me, as I will not be the only one using this and I don’t want to have my kids and wife have to log into another layer to track chores) The others I have seen have parts that I want but not all.
Goals - Operation - I want chores that are supposed to be done every so many days, or daily. I want them to be assigned to people in the house randomly but equally (I think the random integration already in Home assistant can be used for this based off what I have read about this , I can have it select a random number in a range I specify, and it will pick each number roughly equally in quantity to any other number in the range. I figure I can just have each number be equal to a child/member of the household)
Proposed way to make work - I think having each chore be seen as some sort of device with multiple sensors may be the way?
For example, Clean The Bathroom is a chore, it has the following sensors:
Person Assigned = Random number between 1 and 7 - Each number is displayed as a name of a member of the household - Can this possibly be linked to users of my home assistant instance?
Date Done: Last time the chore was marked done
Done by: Last person who did the chore, basically the previous state of the person assigned sensor
Date Due: Next time the chore is due
Input date: When person assigned does the chore, they can mark it done and the chore is automatically re-dated with a new due date and assigned to a new person.

I also assume there will need to be a script and makes the due date sensor reset with a new date and makes the person assigned sensor get a new random number.

So, is this possible with what is already in Home assistant?

1 Like

I think it’s possible. Using Input_datetime, input_number to store dates and person numbers, scripts to change them. Custom header allows for per user view. Custom button card for presenting the chore info.

It will be quite involved…

I have no problem with it being involved. I am going to be having time to develop this…as over the next year I will have more free time than usual. I will use this to not only develop this but hopefully learn much more about making integrations for Home Assistant.

My chore tracking for inspiration:
afbeelding1 afbeelding2

When your chore is done, you can tap the button and it will reset the counter. The chore turns orange then red after a certain amount of days. A long tap opens a ‘More info’ popup where you can adjust the date, so you can backdate if you forgot to ‘register’ your task the same day you performed it.

There are other resources, e.g.:

Plenty of inspiration, and what you are describing is certainly doable. You can just use the default HA tool set (template sensors, input_datetime, etc), or build a custom integration if you’re up for some Python coding.

May i ask so bluntly, to share a bit of your config?. It looks really neath.

Sure. Do you mean just the looks (custom button card config)? Or the whole setup including template sensors etc.?

If possible everything :sweat_smile::joy:

Input_datetime and script:

input_datetime:
  bathroom_last_cleaned:
    name: "Bathroom last cleaned"
    has_date: true

scripts:
  cleaned_bathroom:
    alias: 'Bathroom cleaned'
    sequence:
      - service: input_datetime.set_datetime
        data:
          entity_id: input_datetime.bathroom_last_cleaned
          date: "{{ now().timestamp() | timestamp_custom('%Y-%m-%d', true) }}"

Template sensor:

  - platform: template
    sensors:
      bathroom_last_cleaned:
        friendly_name: 'Bathroom last cleaned'
        unit_of_measurement: 'day(s) ago'
# set x= is for ensuring a daily update of this sensor. No longer necessary from HA 0.117.
        value_template: >
          {% set x = states('sensor.date') %}
          {{ ((now().timestamp() - state_attr('input_datetime.bathroom_last_cleaned','timestamp')) / (3600*24)) | round(0, 'floor') |int }}

Button template for custom_button_card:
(depends for styling on theme variables like var(–border-radius) being available in your theme file, if they’re not available just replace them with appropriate values)

button_chores:
  label: ' '
  aspect_ratio: 1/1
  show_state: true
  show_label: false
  show_name: true
  show_icon: true
  size: 20%
  variables:
    alert_green: 7
    alert_orange: 14
  
  # Default styles
  styles:
    card:
      - border-radius: var(--border-radius)
      - filter: opacity(80%)
      - background-color: >
          [[[
            if (entity.state < variables.alert_green)
              return "green";
            if (entity.state < variables.alert_orange)
              return "orange";
            return "red";
          ]]]
    grid:
      - grid-template-areas: '"i" "n" "s"'
      - grid-template-columns: 1fr
      - grid-template-rows: 1fr min-content min-content
    label:
      - font-size: 11px
      - font-family: Helvetica
      - padding: 0px 10px
      - justify-self: start
      - color: var(--label-color-off)
    state:
      - font-size: 11px
      - font-family: Helvetica
      - padding: 0px 10px
      - justify-self: start
      - font-weight: bold
      - color: var(--upcoming-color)
    img_cell:
      - justify-content: start
      - padding-left: 13%
    name:
      - color: var(--upcoming-color)
      - justify-self: start
      - font-weight: bold
      - font-family: Helvetica
      - font-size: 13px
      - margin-top: 0px
      - margin-left: 0px

Lovelace button:

            - type: custom:button-card
              template: button_chores
              entity: sensor.bathroom_last_cleaned
              name: Bathroom cleaned
              icon: mdi:shower
              aspect_ratio: 2/1
              variables:
                alert_green: 7
                alert_orange: 14
              tap_action:
                action: call-service
                service: script.cleaned_bathroom
              hold_action:
                action: more-info
                entity: input_datetime.bathroom_last_cleaned
2 Likes

Thanks man, got a little variation on it.

image

2 Likes

Nice. I totally forgot about the template sensor. Seems you got it figured out already, but still I added it to my post.

1 Like

No problem, yes i was very curious about your lovelace setup and your button template. The sensor is not that hard.

Thanks again though

Thanks Roelof (@Emphyrio) for sharing your setup! I built on it and made a few changes:

  • get rid of template sensor and script
  • move functionality of buttons into button template

The advantage of the changes is that it now only takes very few lines to add a new chore (add a datetime entity with date only and 8 lines for a new button-card). The disadvantage is that the cards now show the date when the chore was last done and not how long this is ago (e.g. “February 16, 2021” instead of “2 days ago”).

button card template:

button_card_templates:
  button_chores_direct:
    aspect_ratio: 2/1
    show_state: true
    show_label: false
    show_name: true
    show_icon: true
    size: 20%
    variables:
      alert_green: 7
      alert_orange: 14
    tap_action:
      action: call-service
      service: input_datetime.set_datetime
      service_data:
        entity_id: entity
        date: |
          [[[ 
              var d = new Date();
              var year = d.getFullYear();
              var month = d.toLocaleString('en-US', { month : '2-digit' });
              var day = d.toLocaleString('en-US', { day : '2-digit' });
              var result = year + "-" + month + "-" + day;
              return result
          ]]]
    hold_action:
      action: more-info
    styles:
      card:
        - border-radius: var(--border-radius)
        - filter: opacity(100%)
        - background-color: |
            [[[
              if (Date.now()/1000 - entity.attributes.timestamp < variables.alert_green * 24 * 3600)
                return "mediumaquamarine";
              if (Date.now()/1000 - entity.attributes.timestamp < variables.alert_orange * 24 * 3600)
                return "orange";
              return "red";
            ]]]
      grid:
        - grid-template-areas: '"i" "n" "s"'
        - grid-template-columns: 1fr
        - grid-template-rows: 1fr min-content min-content
      label:
        - font-size: 11px
        - font-family: Helvetica
        - padding: 0px 10px
        - justify-self: start
        - color: var(--label-color-off)
      state:
        - font-size: 11px
        - font-family: Helvetica
        - padding: 0px 10px
        - justify-self: start
        - font-weight: bold
        - color: var(--upcoming-color)
      img_cell:
        - justify-content: start
        - padding-left: 13%
      name:
        - color: var(--upcoming-color)
        - justify-self: start
        - font-weight: bold
        - font-family: Helvetica
        - font-size: 13px
        - margin-top: 0px
        - margin-left: 10px

example button card:

          - type: 'custom:button-card'
            template: button_chores_direct
            entity: input_datetime.plants_last_watered
            name: watered plants
            icon: 'mdi:flower'
            variables:
              alert_green: 6
              alert_orange: 13
2 Likes

Really like this solution but unsure how to use the template? Where do i set it and how, i know custom_button_card was removed from HACS

Thanks @dicera and @Emphyrio, this is very helpful.

I’ve tweaked a couple things for my own use that I wanted to share back:

  1. Use relative time for the state (e.g. XXXd ago).
  2. Use sensor.date to get today’s date instead of recalculating
  3. Move the button-card variables for the warning times to entity customizations–this allows them to be read as attributes and also used in automations in order to send notifications.
# This requires the following sensors to be configured somewhere:
# sensor:
# - platform: time_date
#   display_options:
#     - 'time'
#     - 'date'
#
# For each input_datetime use the following customizations:
# homeassistant:
#   customize:
#     input_datetime.some_entity:
#       alert_green: 6
#       alert_orange: 13
button_chores_direct:
  aspect_ratio: 2/1
  show_state: true
  show_label: false
  show_name: true
  show_icon: true
  size: 20%
  triggers_update: sensor.time
  tap_action:
    action: call-service
    service: input_datetime.set_datetime
    service_data:
      entity_id: entity
      date: |
        [[[
            return states['sensor.date'].state;
        ]]]
  hold_action:
    action: more-info
  styles:
    card:
      - border-radius: var(--border-radius)
      - filter: opacity(100%)
      - background-color: |
          [[[
            if (Date.now()/1000 - entity.attributes.timestamp < entity.attributes.alert_green * 24 * 3600)
              return "mediumaquamarine";
            if (Date.now()/1000 - entity.attributes.timestamp < entity.attributes.alert_orange * 24 * 3600)
              return "orange";
            return "red";
          ]]]
    grid:
      - grid-template-areas: '"i" "n" "s"'
      - grid-template-columns: 1fr
      - grid-template-rows: 1fr min-content min-content
    label:
      - font-size: 11px
      - font-family: Helvetica
      - padding: 0px 10px
      - justify-self: start
      - color: var(--label-color-off)
    state:
      - font-size: 11px
      - font-family: Helvetica
      - padding: 0px 10px
      - justify-self: start
      - font-weight: bold
      - color: var(--upcoming-color)
    img_cell:
      - justify-content: start
      - padding-left: 13%
    name:
      - color: var(--upcoming-color)
      - justify-self: start
      - font-weight: bold
      - font-family: Helvetica
      - font-size: 13px
      - margin-top: 0px
      - margin-left: 10px
  state_display: |
    [[[
      const time = c => {
        const s = (c / 1000);
        const m = (c / (1000 * 60));
        const h = (c / (1000 * 60 * 60));
        const d = (c / (1000 * 60 * 60 * 24));
        if (s < 60) {
          return parseInt(s) + 's ago';
        } else if (m < 60) {
          return parseInt(m) + 'm ago';
        } else if (h < 24) {
          return parseInt(h) + 'h ago';
        } else {
          return parseInt(d) + 'd ago';
        }
      };
      let last_changed = entity === undefined || time(Date.now() - Date.parse(entity.state));
      return last_changed;
    ]]]

EDIT: Here’s a simplified version of the state_display template for days only:

  state_display: |
    [[[
      const time = c => {
        const d = Math.floor(c / (1000 * 60 * 60 * 24));
        if (d < 1) {
          return 'Today';
        }
        if (d === 1) {
          return 'Yesterday';
        }
        return `${parseInt(d)} days ago`;
      };
      return entity === undefined || time(Date.now() - Date.parse(entity.state));
    ]]]
1 Like

@bighead85 – Don’t know if you’ve figured this out already, but you need to use the button-card custom Lovelace card. Here is how to use the templates.

I’m really new to this and I want to use this kind of chores tracking, it looks great and will improve my routines. Can someone explain where all this text should go? If I put everything in the configuration.yaml, nothing works. I understand that some of this text should go in the configuration of the button card, but I guess not all of it.

Thanks in advance for your help.

This seems broken since the update to 2023.4 - anyone else has problems with it?

Cannot read properties of null (reading ‘config’) (code for the card)

type: custom:button-card
template: button_chores_direct
entity: input_datetime.bed_verschonen
name: Proper bedje
variables:
  alert_green: 13
  alert_orange: 14

I don’t know what is missing I also checked the logs.

EDIT - SOLVED: there is an update in HACS for the button-card integration, which gets it working again.

1 Like

Hi all,

first of all thanks for the great pieces of shared code to all contributors. I’m currently implementing this myself and since I had some trouble figuring stuff out I wanna share my experience here if people may face the same issues. This is based on @shbatm 's and @dicera 's code so props to them.

I am not sure whether all the names of the Menus I’ll give are correct since my HA-Installation is set to German and so are the menus but I’ll try and give it a good translation so you will hopefully find it.

  1. Requirements:
    You need to have button-card installed

  2. Entities:
    For each chore you want to track you need to create a helper entity of type “input_datetime”. You need at least one to implement a card so I advise to create a dummy one upfront while you experiment. You can change them later. To do so go to settings > Devices & Services > On the top select “Helpers” > On the bottom right click “+ Create Helper” > Select “Date and/or Time” > Give it a name, symbol and make sure the radio button for just “Date” is selected > Click create. Repeat this for every chore you want to track.

  3. Where does which code belong?
    Had to try a bit to figure it out so I wanna share it. Add them step by step in the order I describe because some parts rely on code added in earlier parts. Otherwise things might not work

This part:

sensor:
- platform: time_date
  display_options:
    - 'time'
    - 'date'

goes into you configuration.yaml. If you don’t have any sensors configuerd yet you can just add it at the end. If you already have sensors (look if yo find the word “sensor:” somewhere) you can leave out the first line and just add it to your list of sensors.

This part:

button_chores_direct:
  aspect_ratio: 2/1
  show_state: true
  show_label: false
  show_name: true
  show_icon: true
  size: 20%
  triggers_update: sensor.time
  tap_action:
    action: call-service
    service: input_datetime.set_datetime
    service_data:
      entity_id: entity
      date: |
        [[[
            return states['sensor.date'].state;
        ]]]
  hold_action:
    action: more-info
  styles:
    card:
      - border-radius: var(--border-radius)
      - filter: opacity(100%)
      - background-color: |
          [[[
            if (Date.now()/1000 - entity.attributes.timestamp < entity.attributes.alert_green * 24 * 3600)
              return "mediumaquamarine";
            if (Date.now()/1000 - entity.attributes.timestamp < entity.attributes.alert_orange * 24 * 3600)
              return "orange";
            return "red";
          ]]]
    grid:
      - grid-template-areas: '"i" "n" "s"'
      - grid-template-columns: 1fr
      - grid-template-rows: 1fr min-content min-content
    label:
      - font-size: 11px
      - font-family: Helvetica
      - padding: 0px 10px
      - justify-self: start
      - color: var(--label-color-off)
    state:
      - font-size: 11px
      - font-family: Helvetica
      - padding: 0px 10px
      - justify-self: start
      - font-weight: bold
      - color: var(--upcoming-color)
    img_cell:
      - justify-content: start
      - padding-left: 13%
    name:
      - color: var(--upcoming-color)
      - justify-self: start
      - font-weight: bold
      - font-family: Helvetica
      - font-size: 13px
      - margin-top: 0px
      - margin-left: 10px
  state_display: |
    [[[
      const time = c => {
        const s = (c / 1000);
        const m = (c / (1000 * 60));
        const h = (c / (1000 * 60 * 60));
        const d = (c / (1000 * 60 * 60 * 24));
        if (s < 60) {
          return parseInt(s) + 's ago';
        } else if (m < 60) {
          return parseInt(m) + 'm ago';
        } else if (h < 24) {
          return parseInt(h) + 'h ago';
        } else {
          return parseInt(d) + 'd ago';
        }
      };
      let last_changed = entity === undefined || time(Date.now() - Date.parse(entity.state));
      return last_changed;
    ]]]

goes into the Raw code of the dashboard you want to implement your button cards. On your dashboard click the three dots on the top right > click edit dashboard > click the three dots again > select Raw configuration editor. Then paste this code just at the end of the code window.
Here is the code for simpler date display:

  state_display: |
    [[[
      const time = c => {
        const d = Math.floor(c / (1000 * 60 * 60 * 24));
        if (d < 1) {
          return 'Today';
        }
        if (d === 1) {
          return 'Yesterday';
        }
        return `${parseInt(d)} days ago`;
      };
      return entity === undefined || time(Date.now() - Date.parse(entity.state));
    ]]]

If someone is interested, I also made a version which additionaly shows you when the task was done last, when the task is due next time if it’s not due and how long it is (over)due if it is (over)due. It also breaks the due days down to weeks if it is more than seven and switches between singular and plural for weeks and days (I hope correctly).
Here is a screenshot from my dashboard:


So feel free to use and modify:

    state_display: |
      [[[
        var ago = ''
        var until = ''
        const time = c => 
        {
          const d = Math.floor(c / (1000 * 60 * 60 * 24));
          if (d < 1) 
          {
            ago = 'last today';
          }
          else if (d === 1) 
          {
            ago = 'last yesterday';
          }
          else if (d < 7)
          {
            ago = 'last ' + (d+1) + ' days ago';
          }
          else if (Math.floor(d/7) === 1)
          {
            if ((d-(Math.floor(d/7)*7)) < 1)
            {
              ago = 'last ' + Math.floor(d/7) + ' week ago';
            }
            else if ((d-(Math.floor(d/7)*7)) === 1)
            {
              ago = 'last ' + Math.floor(d/7) + ' week and ' + (d-(Math.floor(d/7)*7)) + ' day ago';
            }
            else
            {
              ago = 'last ' + Math.floor(d/7) + ' week and ' + (d-(Math.floor(d/7)*7)) + ' days ago';
            }
          }
          else
          {
            if ((d-(Math.floor(d/7)*7)) < 1)
            {
              ago = 'last ' + Math.floor(d/7) + ' weeks ago';
            }
            else if ((d-(Math.floor(d/7)*7)) === 1)
            {
              ago = 'last ' + Math.floor(d/7) + ' weeks and ' + (d-(Math.floor(d/7)*7)) + ' day';
            }
            else
            {
              ago = 'last ' + Math.floor(d/7) + ' weeks and ' + (d-(Math.floor(d/7)*7)) + ' days';
            }
          }
          if (Date.now()/1000 - entity.attributes.timestamp < entity.attributes.alert_green * 24 * 3600)
          {
            var calc = Math.floor(((entity.attributes.timestamp + (entity.attributes.alert_green * 24 * 3600)) - Math.floor(Date.now()/1000))/(60 * 60 * 24))
            if (calc < 1) 
            {
              until = 'due tomorrow';
            }
            else if (calc < 7)
            {
              until = 'due in ' + (calc+1) + ' days';
            }
            else if (Math.floor(calc/7) === 1)
            {
              if ((calc-(Math.floor(calc/7)*7)) < 1)
              {
                until = 'due in ' + Math.floor(calc/7) + ' week';
              }
              else if ((calc-(Math.floor(calc/7)*7)) === 1)
              {
                until = 'due in ' + Math.floor(calc/7) + ' week and ' + (calc-(Math.floor(calc/7)*7)) + ' day';
              }
              else
              {
                until = 'due in ' + Math.floor(calc/7) + ' week and ' + (calc-(Math.floor(calc/7)*7)) + ' days';
              }
            }
            else
            {
              if ((calc-(Math.floor(calc/7)*7)) < 1)
              {
                until = 'due in ' + Math.floor(calc/7) + ' weeks';
              }
              else if ((calc-(Math.floor(calc/7)*7)) === 1)
              {
                until = 'due in ' + Math.floor(calc/7) + ' weeks and ' + (calc-(Math.floor(calc/7)*7)) + ' day';
              }
              else
              {
                until = 'due in ' + Math.floor(calc/7) + ' weeks and ' + (calc-(Math.floor(calc/7)*7)) + ' days';
              }
            }
          }
          else if (Date.now()/1000 - entity.attributes.timestamp < entity.attributes.alert_orange * 24 * 3600)
          {
            var calc = Math.floor((Math.floor(Date.now()/1000) - entity.attributes.timestamp - entity.attributes.alert_green * 24 * 3600)/(60 * 60 * 24))
            if (calc < 1) 
            {
              until = 'due since today';
            }
            else if (calc === 1) 
            {
              until = 'due since yesterday';
            }
            else if (calc < 7) 
            {
              until = 'due for ' + calc + ' days';
            }
            else if (Math.floor(calc/7) === 1)
            {
              if ((calc-(Math.floor(calc/7)*7)) < 1)
              {
                until = 'due for ' + Math.floor(calc/7) + ' week';
              }
              else if ((calc-(Math.floor(calc/7)*7)) === 1)
              {
                until = 'due for ' + Math.floor(calc/7) + ' week and ' + (calc-(Math.floor(calc/7)*7)) + ' day';
              }
              else
              {
                until = 'due for ' + Math.floor(calc/7) + ' week and ' + (calc-(Math.floor(calc/7)*7)) + ' days';
              }
            }
            else
            {
              if ((calc-(Math.floor(calc/7)*7)) < 1)
              {
                until = 'due for ' + Math.floor(calc/7) + ' weeks';
              }
              else if ((calc-(Math.floor(calc/7)*7)) === 1)
              {
                until = 'due for ' + Math.floor(calc/7) + ' weeks and ' + (calc-(Math.floor(calc/7)*7)) + ' day';
              }
              else
              {
                until = 'due for ' + Math.floor(calc/7) + ' weeks and ' + (calc-(Math.floor(calc/7)*7)) + ' days';
              }
            }
          }
          else
          {
            var calc = Math.floor((Math.floor(Date.now()/1000) - entity.attributes.timestamp - entity.attributes.alert_orange * 24 * 3600)/(60 * 60 * 24))
            if (calc < 1) 
            {
              until = 'overdue since today';
            }
            else if (calc === 1) 
            {
              until = 'overdue since yesterday';
            }
            else if (calc < 7) 
            {
              until = 'overdue for ' + calc + ' days';
            }
            else if (Math.floor(calc/7) === 1)
            {
              if ((calc-(Math.floor(calc/7)*7)) < 1)
              {
                until = 'overdue for ' + Math.floor(calc/7) + ' week';
              }
              else if ((calc-(Math.floor(calc/7)*7)) === 1)
              {
                until = 'overdue for ' + Math.floor(calc/7) + ' week and ' + (calc-(Math.floor(calc/7)*7)) + ' day';
              }
              else
              {
                until = 'overdue for ' + Math.floor(calc/7) + ' week and ' + (calc-(Math.floor(calc/7)*7)) + ' days';
              }
            }
            else
            {
              if ((calc-(Math.floor(calc/7)*7)) < 1)
              {
                until = 'overdue for ' + Math.floor(calc/7) + ' weeks';
              }
              else if ((calc-(Math.floor(calc/7)*7)) === 1)
              {
                until = 'overdue for ' + Math.floor(calc/7) + ' weeks and ' + (calc-(Math.floor(calc/7)*7)) + ' day';
              }
              else
              {
                until = 'overdue for ' + Math.floor(calc/7) + ' weeks and ' + (calc-(Math.floor(calc/7)*7)) + ' days';
              }
            }
          }
          return '<DIV align="left">' + ago + '<br/>' + until + '</DIV>'
        };
        return entity === undefined || time(Date.now() - Date.parse(entity.state));
      ]]]

Sure there is some potential to shorten this down but I didn’t have the time yet.

Here comes the part I am not sure of. Maybe @shbatm or someone else can explan this:

# For each input_datetime use the following customizations:
homeassistant:
  customize:
  input_datetime.entity_name:
    alert_green: 6
    alert_orange: 13

I guess this goes into configuration.yaml. At least this wors for me. This adds the variables alert_green and alert_orange to your helper. These values how many days the card will stay green and orange respectively and can be set ther. You need to do this for every helper you create. However these variables are also passed in the configuration of the card in the next step. So do we need them twice? It works for me if I just leave it out of the card config card but it seems kind of annoying that you have to restart HA every time to save the changes in configuration.yaml. So any help is welcome.

Finally this part:

type: custom:button-card
template: button_chores_direct
entity: input_datetime.entity_name
name: Chore name
variables:
  alert_green: 13
  alert_orange: 14

goes into the code of the button card. On your normal dashboard editor click “+ Add card” on the bottom right and search for “button-card” and select it. This will open a new window with a small code editor to the left. Paste the code there. The first two lines can stay as they are. In line three you need to put in the name of the entity created earlier. They are always called input_datetime.something. You can look up the correct entity ID in your list of helpers from step 2.Line four has the chore name. I think this explains itself.
For the variables I am not sure as I wrot in the previous part. They seem to be optional since they are set in configuration.yaml. However can I override them here? Does not seem so for me. Is there a way to somehow define the variables globally and overwrite it via the card setting? Maybe someone can help with this.

So maybe someone can help me with my open questions and I hope I can help people who are maybe stuck while trying to get this work.

3 Likes

Nice work, Fennek88!

You don’t have to restart over and over again, if you have it defined in your configuration.yaml:


homeassistant:
  name: !secret home_location
  bla …
  bla…
  customize: !include customize.yaml


Then you are able to reload customizations („Anpassungen“) in Developer Tools/Yaml. Please note, however, that the customizations are not taken into account until the entity has updated.

I don’t see why it has to be used twice as custom attribute and as variable.

Thanks @Fennek88 for the great work (as well as the rest of the contributors in this thread)! I’ve implemented this with my own tweaks and it’s been working beautifully. One tweak I thought I’d share is a condensed version of Fennek88’s date logic. This came about with the help of a developer friend and AI. Everything in this logic is in days, so there is no “Overdue for 2 weeks and 3 days”, just “Overdue for 17 days”.

      state_display: |
        [[[
          const formatDate = (lastCompleted, frequency) => {
          const date = new Date(lastCompleted * 1000);
          const currentDate = new Date();
          const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
          const month = months[date.getMonth()];
          const date2 = date.getDate().toString();
          let ago = '';
          let until = '';

          const calculateDays = (time1, time2) => Math.floor((time1 - time2 ) / (60 * 60 * 24));

          const isSameDay = (date1, date2) => date1.getDate() === date2.getDate() && date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear();

          if (isSameDay(date, currentDate)) {
            ago = 'Completed today';
          } else if (isSameDay(new Date(currentDate.getTime() - 86400000), date)) {
            ago = 'Completed yesterday';
          } else {
            ago = 'Last completed ' + month + ' ' + date2;      
          }

          const calc = frequency - calculateDays(Date.now() / 1000, lastCompleted);

          if (calc < 0) {
            until = 'Overdue for ' + Math.abs(calc) + ' days';
          } else if (calc === 0) {
            until = 'Due today';
          } else if (calc === 1) {
            until = 'Due tomorrow';
          } else if (calc < 7) {
            until = 'Due in ' + calc + ' days';
          } else if (Math.floor(calc / 7) === 1) {
            until = calc - Math.floor(calc / 7) * 7 < 1 ? 'Due in ' + Math.floor(calc / 7) + ' week' :
              (calc - Math.floor(calc / 7) * 7 === 1 ? 'Due in ' + Math.floor(calc / 7) + ' week and a day' :
                'Due in ' + Math.floor(calc / 7) + ' week and ' + (calc - Math.floor(calc / 7) * 7) + ' days');
          } else {
            until = calc - Math.floor(calc / 7) * 7 < 1 ? 'Due in ' + Math.floor(calc / 7) + ' weeks' :
              (calc - Math.floor(calc / 7) * 7 === 1 ? 'Due in ' + Math.floor(calc / 7) + ' weeks and a day' :
                'Due in ' + Math.floor(calc / 7) + ' weeks and ' + (calc - Math.floor(calc / 7) * 7) + ' days');
          }

          return '<DIV align="left">' + ago + '<br/>' + until + '</DIV>';
        };

        return formatDate(entity.attributes.timestamp, variables.frequency);
        ]]]