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.

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.

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.

How to set up your own

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

Create the list of chores in a YAML file, such as chores.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:

input_button chores: !include config/chores.yaml
input_text chores: !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".

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

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:
  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
  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 }
    ]]]
  custom_fields:
    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
      - 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
    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
    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)
  tap_action:
    action: call-service
    service: input_button.press
    service_data:
      entity_id: entity
  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.chore_plants
        options:
          type: 'custom:button-card'
          template: chore_card
 card:
   type: entities
   title: Plants

Refresh your dashboard, and your chore list is complete.

How to set up a summary of chores