Skip to content

Super Simple Chore Tracker for Home Assistant

Madelena Mak edited this page Jul 2, 2022 · 16 revisions

What is it

Under the Maintenance tab, I kept a list of chores, as shown in the middle column in this screenshot:

HA Chores List

I made it the simplest way I could with what I knew back then since I didn't know how to script (and still doesn't).

This is designed for recurring chores with soft deadlines, as in "X amount of days since it was last done". Like watering plants, I can't do it on a strict daily or weekly schedule - I will end up overwatering it. The best I can do is to monitor the condition and do it as needed, but I should be notified if it has been a while since it was last done.

Here's the thing - a little bit on the natures of chores. Every household has their own 'laws' on when chores should be done, and there isn't a one-size-fits-all chore solution as far as I can find:

  • Recurring chores with hard deadlines (e.g. "Get the exterminator every July 1st"), which is better handled by a calendar;
  • One time tasks (e.g. "Fix overflowing basement toilet") which is better handled by a To Do list app;
  • Habits which requires a task done within a hard regular interval (e.g. "Weight training every week") which is better handled by habit apps; or
  • Collaborative chores with multiple people involved (e.g. "Refurnish the entire kitchen together") which in my opinion a Trello board is better suited.

I also don't care who did a chore, so all I needed to know is whether I haven't done a chore for too long. There are no assignees function in this tracker.

How it works

Basically, all the logic is handled by the front end. The button-cards do the date calculations of whether a task is due or not, by comparing the last changed date of an input button with the time interval set by the corresponding input text.

Each chore has 4 states: Overdue, To Do Soon, Normal, and Done. When a chore is selected, it will reset the last done time to the current moment, and clear any Overdue or To Do Soon status and replace it with Done until the end of the day.

Change the amount of days to count since the chore is last done by long pressing a chore card to bring up the more-info card. Put any numbers for the amount of days, and put "-1" if the chore is only done as needed.

HA Chore List - Counter

How to set up

Difficulty Level: Intermediate. Some basic coding skills required.

Since this involves coding, use a code editor such as the Studio Code Server add-on for Home Assistant.

First, install button-card to your frontend: https://github.com/custom-cards/button-card

Create the list of chores in a chores.yaml and save it in the same folder as your configurations.yaml:

chore_clean_kitchen_sink:     # A unique ID for the chore
  name: Clean Kitchen Sink    # The name of the chore to be shown on the Chore List
  icon: mdi:water-pump        # The icon of the chore to be shown on the Chore List
chore_clean_kitchen_counter:
  name: Clean Kitchen Counter
  icon: mdi:countertop
chore_clean_kitchen_stove:
  name: Clean Kitchen Stove
  icon: mdi:stove

In configuration.yaml, include the chores.yaml file in configuration.yaml by adding these two lines:

input_button: !include config/chores.yaml
input_text: !include config/chores.yaml

Then create an automation to put all the chores automatically in a group every time Home Assistant starts:

- alias: Home Assistant starts ➔ Update Group - Chores
  trigger:
  - platform: homeassistant
    event: start
  - platform: event
    event_type: call_service
    event_data:
      domain: group
      service: reload
  action:
  - service: group.set
    data_template:
      name: All Chores
      object_id: chores
      entities: |
        {% set ns = namespace(entities=[]) %}
        {% for s in states.input_button if s.object_id.startswith('chore_') %}
          {% set ns.entities = ns.entities + [ s.entity_id ] %}
        {% endfor %}
        {{ ns.entities }}

Validate your configuration in HA Developer Tools, and then reload the YAML configuration for "Input Buttons", "Input Texts", and then once those two are reloaded, reload "Groups, Group Entities, and Notify Services".

HA Chores - Step 1

Double check that the chores are loaded in the States tab of Developer Tools by searching for the entities such as group.chores. There should be a list of your chores in the attributes of the entity.

HA Chores - Step 2 HA Chores - Step 2b

The last steps involve integrating the chores to the front end.

Add a template for a chore item for button-card. Put the following code under button_card_templates: in your dashboard YAML. Check the button-card ReadMe for more info.

chore_card:

  # Set up variables for calculating chore due dates.
  variables:
    now: >-
      [[[ return new Date() ]]]
    last_done: >-
      [[[ return new Date(entity.state) ]]]
    diff: >-
      [[[ return Math.round((new Date() - new Date(entity.state)) / 1000 / 60 / 60 / 24 ) ]]]
    due: >-
      [[[ return states[entity.entity_id.replace("input_button", "input_text")].state ]]]

  triggers_update: all
  show_label: true
  layout: icon_name_state2nd
  size: 24px

  # Show when a chore is due and when it was last done.
  label: |
    [[[
      var doneStr
      if (variables.diff < 2) {
        if (variables.last_done.getDay() == variables.now.getDay()) { doneStr = 'today' } else { doneStr = 'yesterday' }
      } else if (isNaN(variables.diff)) {
        doneStr = 'unknown'
      } else {
        doneStr = variables.diff + ' days ago'
      }
      if (variables.due > 0) {
        return 'Every ' + variables.due + ' days &bull; Last done ' + doneStr
      } else { return 'As needed &bull; Last done ' + doneStr }
    ]]]

  # Show the chore status as a badge.
  custom_fields:
    badge: &field-chore-badge |
      [[[
        if ((variables.diff < 2) && (variables.last_done.getDay() == variables.now.getDay())) { return 'Done' }
        if (variables.due > 0) {
          if (variables.due < variables.diff) { return 'Overdue' }
          if (variables.due < variables.diff + (variables.due / 4)) { return 'To do soon' }
        }
      ]]]

  styles:
    card:
      - margin: 4px 0
      - padding: 4px 12px

      # Show a subtle background color depending on the chore status.
      - background-color: |
          [[[
            if ((variables.diff < 2) && (variables.last_done.getDay() == variables.now.getDay())) { return 'rgba(var(--rgb-success-color), .33)' }
            if ((variables.due > 0) && (variables.due < variables.diff)) { return 'rgba(var(--rgb-error-color), .33)' } else {return 'transparent'}
          ]]]

    grid:
      - grid-template-columns: min-content 1fr min-content
      - grid-template-areas: '"i n badge" "i s badge" "i l badge"'

    img_cell:
      - align-self: middle
      - text-align: start
      - padding: 8px 24px 8px 4px

    # Colorize the icon depending on the chore status.
    icon:
      - color: |
          [[[
            if ((variables.diff < 2) && (variables.last_done.getDay() == variables.now.getDay())) { return 'var(--success-color)' }
            if (variables.due > 0) {
              if (variables.due < variables.diff) { return 'var(--error-color)' }
              if (variables.due < variables.diff + (variables.due / 4)) { return 'var(--warning-color)' }
            } else { return 'var(--primary-text-color)' }
          ]]]

    name:
      - align-self: middle
      - justify-self: start
      - font-size: var(--body-font-size)

    label:
      - align-self: middle
      - justify-self: start
      - font-size: var(--body-font-size)
      - opacity: 0.66

    # Colorize the badge depending on the chore status.  
    custom_fields:
      badge:
        - background: |
            [[[
              if ((variables.diff < 2) && (variables.last_done.getDay() == variables.now.getDay())) { return 'var(--success-color)' }
              if (variables.due < variables.diff) { return 'var(--error-color)' } else { return 'var(--warning-color)' }
            ]]]
        - padding: 2px
        - line-height: 1
        - font-size: var(--h6-font-size)
        - font-weight: 900
        - text-transform: uppercase
        - border-radius: 2px
        - color: var(--accent-text-color)

  # Short press a chore card to set it as Done.
  tap_action:
    action: call-service
    service: input_button.press
    service_data:
      entity_id: entity

  # Long press a chore card to change the amount of days a chore is due at.
  hold_action:
    action: more-info
    entity: >-
      [[[ return entity.entity_id.replace("input_button", "input_text") ]]]

Finally, add the list of chores to your dashboard. The auto-entities frontend module will make things easier if you have a lot of chores, but is optional.

- type: 'custom:auto-entities'
  filter:
    include:
      - group: group.chores
        options:
          type: 'custom:button-card'
          template: chore_card
 card:
   type: entities
   title: Chores

Refresh your dashboard, and your chore list is complete.

How to set up a summary of chores

Create template sensors to count the number of chores to be done:

template:

- trigger:
    - platform: homeassistant
      event: start
    - platform: event
      event_type: event_template_reloaded
    - platform: time_pattern
      minutes: "/5"
    - platform: state
      entity_id:
        - group.chores
  sensor:

    - name: Chores Done
      unit_of_measurement: "Tasks"
      state: "OK"
      attributes:
        entities: >
          {%- set chore_done = namespace(entities=[]) -%}
          {%- for chore in expand('group.chores') -%}
            {%- if (chore.state != 'unknown') -%}
              {%- set input = chore.entity_id|regex_replace(find='input_button', replace='input_text') -%}
              {%- set diff = ( as_timestamp(now()) - as_timestamp(chore.state) ) / 60 / 60 / 24 %}
              {%- set due = states(input)|float(-1) -%}
              {%- if (diff < 1) and (as_datetime(chore.state).day == now().day) -%}
                {%- set chore_done.entities = chore_done.entities + [chore.entity_id] -%}
              {%- endif -%}
            {%- endif %}
          {%- endfor -%}
          {{ chore_done.entities }}

    - name: Chores To Do
      state: "OK"
      attributes:
        entities: >
          {%- set chore_todo = namespace(entities=[]) -%}
          {%- for chore in expand('group.chores') -%}
            {%- if (chore.state != 'unknown') -%}
              {%- set input = chore.entity_id|regex_replace(find='input_button', replace='input_text') -%}
              {%- set diff = ( as_timestamp(now()) - as_timestamp(chore.state) ) / 60 / 60 / 24 %}
              {%- set due = states(input)|float(-1) -%}
              {%- if due > 0 -%}
                {%- if (due < diff) -%}
                {%- else -%}
                  {%- if (due < diff + (due / 4)) -%}{%- set chore_todo.entities = chore_todo.entities + [chore.entity_id] -%}{%- endif -%}
                {%- endif -%}
              {%- endif -%}
            {%- endif %}
          {%- endfor -%}
          {{ chore_todo.entities }}

    - name: Chores Overdue
      state: "OK"
      attributes:
        entities: >
          {%- set chore_overdue = namespace(entities=[]) -%}
          {%- for chore in expand('group.chores') -%}
            {%- if (chore.state != 'unknown') -%}
              {%- set input = chore.entity_id|regex_replace(find='input_button', replace='input_text') -%}
              {%- set diff = ( as_timestamp(now()) - as_timestamp(chore.state) ) / 60 / 60 / 24 %}
              {%- set due = states(input)|float(-1) -%}
              {%- if due > 0 -%}
                {%- if (due < diff) -%}
                  {%- set chore_overdue.entities = chore_overdue.entities + [chore.entity_id] -%}
                {%- endif -%}
              {%- endif -%}
            {%- endif %}
          {%- endfor -%}
          {{ chore_overdue.entities }}