Chores - Keep track using HA

First, let me clarify the following :

  • I’m not a developper, and for those of you who are, what you are about to read might seem like absolute nonsense.
  • This post is heavily inspired by lovelace check button card. I’d recommend using this one if stability and reliability is what you are after.
  • Everything is made possible because of lovelace custom button card, which to me is the equivalent of the philosopher’s stone for my lovelace UI.
  • English is (obviously) not my native language. Pardon my french.

Now, into the subject itself.

A bit a backstory : I have a tendancy to procrastinate and need constant reminders to do most of the household chores & maintenance. HA’s been a huge help for me, and I’ve been tweaking my config for about a year now in order to help me get stuff done. For example, I now have several sensors that alert me when one of my plant is thirsty, too cold or require more light. No more dying ones !

But I still needed to find a way to remind me that, from now and then, I should get rid of the dust that lays on top of my kitchen furnitures. Here comes my take on chore management :


note: this is my ‘dev’ instance of HA. What you are seing is, thank god, not the state of the house. The layout is also messy but on this one, I really don’t care.

Thinking UX.
Here’s my thought process :

  • I’m not the only one that’s using this, so it must be very easy to use.
  • Since we are dealing with chores, the process must also be quick because I don’t want to click four times to mark a chore as done when I’m cleaning the litterbox.
  • I need to know precisely what the priorities are.
  • Each chore must have a few parameters that can be set on the fly.

Regarding parameters; I defined two threshold :

  • warning_after that corresponds to the number of days after which the chores enters the warning state (meaning ‘hey, you should think of doing this one’).
  • critical_after that corresponds to the number of days after which the chores enters the critical state (meaning ‘hey, you must do this one’).

Thinking UI.
To keep things short :

  • I’m using button-cards & vertical-stack-in-card.
  • Chores are sorted by rooms/areas. One vertical-stack per room.
  • Each chore can be styled according to its state : never done (light grey), ok (green), warning (orange) and critical (red).

Usage
Each time chore is done, the user clicks its name in the UI.
image
A confirmation message pops up (I’m trying to avoid accidental clicks) :
image
After confirmation, the chore is updated :
image
Settings
Each chore button grants access to the chore settings with an hold action (+ browser mod) :
image
the UI is very much still WIP

User can define warning_after and critical_after parameters, as well as update the date and time on which the chore was done (pressing the chore button updates with the current timestamp).

The update is applied when the users press the button located at the bottom of the card (which at the moment is very much unoticable).

HA config
If additionnal details are needed, I’ll provide but for the moment let me be brief. I defined the following entities in HA:

  • input_number (x2) : input_number.chore_warning & input_number.chore_critical
  • input_datetime : input_datetime.chore_date
  • scripts : chore_update, chore_update_settings, chore_get_settings (more on that later).
  • MQTT sensors : one for each chore. See below.
---
# ------------------------------------------------------------------------------
# File: packages/chores.yaml
# Desc: Sensors that are used to track chores
#
# Content:
# 0. YAML anchors
# 1. Input number
# 2. Input datetime
# 3. MQTT sensors
# 4. Scripts
# ------------------------------------------------------------------------------
# 0. Anchors
homeassistant:
  customize:
    package.node_anchors:
      chore_sensor_mqtt_config: &chore_sensor_mqtt_config
        json_attributes_template: '{{ value_json.attributes | tojson }}'
        platform: mqtt
        unit_of_measurement: timestamp
        value_template: '{{ value_json.value }}'
# ------------------------------------------------------------------------------
# 1. Input numbers
input_number:
  # Defines when a chore enters the warning state
  chore_warning:
    min: 1
    max: 100
    step: 1
    unit_of_measurement: 'jour(s)'
    mode: box
  # Desinfes when a chore enters the critical state
  chore_critical:
    min: 1
    max: 365
    step: 1
    unit_of_measurement: 'jour(s)'
    mode: box
# ------------------------------------------------------------------------------
# 2. Input datetime
input_datetime:
  chore_date:
    has_date: true
    has_time: true
# ------------------------------------------------------------------------------
# 3. MQTT sensors (sorted by rooms)
sensor:
  # Bedroom --------------------------------------------------------------------------------------------------------------------------------
  # Clean floor
  - state_topic: 'home/chores/chores_bedroom_clean_floor'
    name: chores_bedroom_clean_floor
    json_attributes_topic: 'home/chores/chores_bedroom_clean_floor'
    <<: *chore_sensor_mqtt_config
  # Clean dust
  - state_topic: 'home/chores/chores_bedroom_clean_dust'
    name: chores_bedroom_clean_dust
    json_attributes_topic: 'home/chores/chores_bedroom_clean_dust'
    <<: *chore_sensor_mqtt_config
# ------------------------------------------------------------------------------------------------------------------------------------------
# 4. Scripts
script:
  # Update chore value ---------------------------------------------------------------------------------------------------------------------
  # This will update the sensor value (timestamp when the chosre was done) and keep its settings.
  chore_update:
    sequence:
      - data_template:
          topic: '{{ topic }}'
          payload: '{ "value": {{ now().timestamp() | int }}, "attributes": { "critical_after" : {{ critical_after }} , "warning_after": {{ warning_after }} }}'
          retain: true
        service: mqtt.publish
  # Update chore settings ------------------------------------------------------------------------------------------------------------------
  # This will keep the sensor value (timestamp when the chosre was done) and update its settings.
  chore_update_settings:
    sequence:
      - data_template:
          topic: '{{ topic }}'
          payload: '{ "value": {{ states.input_datetime.chore_date.attributes.timestamp | int }}, "attributes": { "warning_after": {{ states.input_number.chore_warning.state | int }}, "critical_after": {{ states.input_number.chore_critical.state | int }} }}'
          retain: true
        service: mqtt.publish
  # Get chore settings ---------------------------------------------------------------------------------------------------------------------
  # This will trigger a popup that displays chore settings and a button to update them.
  chore_get_settings:
    sequence:
    # Set input number value according to chore sensor attributes
    - data_template:
        entity_id: input_number.chore_warning 
        value: '{{ warning_after }}'
      service: input_number.set_value
    - data_template:
        entity_id: input_number.chore_critical 
        value: '{{ critical_after }}'
      service: input_number.set_value
    # Set input date value according to chore sensor value
    - service: input_datetime.set_datetime
      data_template:
        entity_id: input_datetime.chore_date
        date: >
          {{ choreTs | timestamp_custom("%Y-%m-%d", true) }}
        time: >
          {{ choreTs | timestamp_custom("%H:%M:%S", true) }}
    # Fire a popup that displays chore settings
    - service: browser_mod.popup
      data_template:
        card:
          type: "custom:vertical-stack-in-card"
          cards:
          - type: entities
            entities:
              - entity: input_number.chore_warning
                icon: 'mdi:alert-outline'
                name: 'Alerter après (jours)'
              - entity: input_number.chore_critical
                icon: 'mdi:car-brake-alert'
                name: 'Critique après (jours)'
              - entities:
                  - entity: input_datetime.chore_date
                    name: 'Effectué le'
                head:
                  type: section
                  label: Réglages avancés
                type: custom:fold-entity-row
          - type: "custom:button-card"
            name: 'Mise à jour de la tâche'
            show_name: true
            tap_action:
              action: call-service
              service: script.chore_update_settings
              service_data:
                topic: '{{ topic }}'
        title: "Paramétrer une tâche"
        deviceID: ["{{ ','.join(deviceID) }}"]

MQTT sensors.
I used mqtt sensors, because I don’t know better. After doing some research, I’ve found they are the easiest to update and work with for what I’m trying to do.

Each sensor contains the following data :

  • State -> timestamp : The last time the chore was done.
  • Attribute - warning_after -> number : The number of days after which the chore enters the warning state.
  • Attribute - critical_after -> number : The number of days after which the chore enters the critical state.

Button config.
This is where things start to get ugly : I haven’t found a way to create an mqtt sensors with attributes on which I can use templating. This means I’ve only found a way to store the number of days after which a chore is deemed critical, but not if it’s in a critical state right now.

Hence, all checks are done in the “front-end”, using JS in the button confirguration. I also haven’t found a way to access custom properties value inside JS templates, so blocks of codes are just copy/paste … this one is ugly, be prepared !

# lovelace_gen
entity: '{{ "sensor." ~ chore }}'
icon: >
  [[[
    var tsSensor = parseInt(entity.state) * 1000;
    var daysSinceLastUpdate = Math.floor(Math.abs(new Date() - new Date(tsSensor)) / 86400000);
    var criticalAfter = (entity.attributes.critical_after ? entity.attributes.critical_after : -1);
    var warningAfter = (entity.attributes.warning_after ? entity.attributes.warning_after : -1);

    var warningIn = warningAfter - daysSinceLastUpdate;
    var criticalIn = criticalAfter - daysSinceLastUpdate;

   if ( warningIn > 0 ) {
      return 'mdi:checkbox-marked-circle-outline';
    } else if ( criticalIn > 0 ) {
      return 'mdi:alert-outline';
    } else {
      return 'mdi:alert-circle';
    }

    return 'help-circle-outline';
  ]]]
custom_fields:
  status: >
    [[[
      var tsSensor = parseInt(entity.state) * 1000;
      var daysSinceLastUpdate = Math.floor(Math.abs(new Date() - new Date(tsSensor)) / 86400000);
      var criticalAfter = (entity.attributes.critical_after ? entity.attributes.critical_after : -1);
      var warningAfter = (entity.attributes.warning_after ? entity.attributes.warning_after : -1);

      var warningIn = warningAfter - daysSinceLastUpdate;
      var criticalIn = criticalAfter - daysSinceLastUpdate;

      if ( criticalAfter < 0 || warningAfter < 0 ) { 
        return '<span style="display: inline-block; color: white; background: var(--disabled-text-color); padding: 0 5px; border-radius: 5px;">Pas fait</span>';
      } else if (warningIn > 0) {
        return '';
      } else if (criticalIn > 0) {
        return '<span style="display: inline-block; color: white; background: var(--accent-color); padding: 0 5px; border-radius: 5px;">' + (criticalIn) + ' jours restant</span>';
      } else {
        return '<span style="display: inline-block; color: white; background: var(--error-color); padding: 0 5px; border-radius: 5px;">Critique</span>';
      }
    ]]]
label: >
  [[[
    var tsSensor = parseInt(entity.state) * 1000;
    var dateToday = new Date().getDate() ;
    var dateSensor = new Date(tsSensor).getDate() ;
    var daysSinceLastUpdate = Math.floor(Math.abs(new Date() - new Date(tsSensor)) / 86400000);

    if ( dateSensor === dateToday && daysSinceLastUpdate === 0) {
      return "Fait aujourd'hui";
    } else if ( dateSensor < dateToday && daysSinceLastUpdate === 0) {
      return "Fait hier";
    } else if ( daysSinceLastUpdate > 0) {
      return "Fait il y a " + daysSinceLastUpdate + " jour" + (daysSinceLastUpdate === 1 ?  "" : "s");
    }
  ]]]
name: '{{ name }}'
show_label: true
styles:
  grid:
    - grid-template-areas: '"i n status" "i l status"'
    - grid-template-columns: 15% 1fr 1fr
    - grid-template-rows: 1fr 1fr
  icon:
    - color: >
        [[[
          var tsSensor = parseInt(entity.state) * 1000;
          var daysSinceLastUpdate = Math.floor(Math.abs(new Date() - new Date(tsSensor)) / 86400000);
          var criticalAfter = (entity.attributes.critical_after ? entity.attributes.critical_after : -1);
          var warningAfter = (entity.attributes.warning_after ? entity.attributes.warning_after : -1);

          var warningIn = warningAfter - daysSinceLastUpdate;
          var criticalIn = criticalAfter - daysSinceLastUpdate;

          if ( criticalAfter < 0 || warningAfter < 0 ) { 
            return 'var(--disabled-text-color)';
          } else if ( warningIn > 0 ) {
            return 'var(--lumo-success-color)';
          } else if ( criticalIn > 0 ) {
            return 'var(--accent-color)';
          } else {
            return 'var(--error-color)';
          }
        ]]]
  label:
    - color: var(--disabled-text-color)
    - justify-self: start
  name: 
    - justify-self: start
tap_action:
  action: call-service
  confirmation:
    text: '[[[ return `Mettre à jour la corvée` ]]]'
  service: script.chore_update
  service_data:
    topic: '{{ "home/chores/" ~ chore }}'
    critical_after: >
      [[[
        return (entity.attributes.critical_after ? entity.attributes.critical_after : 14)
      ]]]
    warning_after: >
      [[[
        return (entity.attributes.warning_after ? entity.attributes.warning_after : 7)
      ]]]
hold_action:
  action: call-service
  service: script.chore_get_settings
  service_data:
    warning_after: >
      [[[
        return (entity.attributes.warning_after ? entity.attributes.warning_after : 14)
      ]]]
    choreTs: >
      [[[
        return (Number.isInteger(parseInt((entity.state))) ? parseInt(entity.state) : Math.floor(new Date().getTime() / 1000))
      ]]]
    critical_after: >
      [[[
        return (entity.attributes.critical_after ? entity.attributes.critical_after : 14)
      ]]]
    deviceID:
      - this
    topic: '{{ "home/chores/" ~ chore }}'
type: "custom:button-card"

Templating
I’m tracking somewhere around 30 chores. There’s no way I’m gonna create 30 scripts, configs & inputs to monitor everything. As you see, Lovelace_gen has been of huge help to me !

… I realize this is getting quite long, I should wrap things up and discuss details later on.

Current state & future improvements
For now, it works. It works well I must admit.

Things that needs to be done though :

  • There’s no way I’m leaving the button card config as it is: it’s messy and not maintanable.
  • I should find a way to store chore state in my mqtt sensors without relying on updating said sensors constantly.
  • I might create a custom component that could be of better use, but my knowledge in reat/angular (that’s what HA’s comps are using, right ?) is very limited so I rely on what I’m most familair with : tinkering with stuff made by others.

Anyway, thanks for reading, this was just a brief overview and I’m sure it needs clarification. I’ll be happy to answer your questions if needed.

For now … let me get back to doing my chores !

Cheers

18 Likes

Great job, love the way this looks!

1 Like

Great work, looks really good! I appreciate your work and might steal some ideas from you :shushing_face:

I wrote a small AppDaemon app also based on the check-button card. You can set the reminder time and after how many days it should send a reminder. The app then sends an actionable notification everyday at the set reminder time if the task hasn’t been done for x days. When you press the button “done” on the actionable notification, it automatically updates the task with the current time.

Let me know in case you are interested in this or in case you want to somehow integrate it into your project. I’ll do it anyway :crazy_face:

Thanks for your feedback ! That sounds like a good idea :slight_smile: ! Currently, I’m using Telegram for all my actionnable notifications :
image
Dummy alert that I triggered

Thing is, in the current situation, I haven’t found a way to determine if a chore is in a warning or critical state, without using a second sensor (not so nice) or updating the sensor value every X minutes. So there’s no way to trigger any automation right now :confused:

What I’d like to set up is a “dynamic” (for lack of a better word) attribute, which would make the sensor look like :

<PSEUDO CODE>
state : ts_last_updated
attributes : {
  warning_after : 7
  warning_status : (difference in days between entity.state and now() > entity.attributes.warning_after ? true : false )}
}

I’m open to suggestions !

1 Like

My app is triggered every day at the reminder time (you can set a reminder time for each chore, could also create an input_datetime to choose the time over the UI) and checks if the task is in a critical/warning state by checking the date of the sensor against the current date. I could also change the app to trigger when a person arrives at home (For me it is like this, but I’m doing it differently, I add all messages that arrive when I’m not home to a “briefing list” and they will be sent to me once I arrive home)

Something like:

If (sensor date + alert days) < today -> send reminder

Your English is better than most Americans.

2 Likes

@Vlavonvidden - just wow, you’ve done an amazing job! I have a dev background and it took me way too long to understand what you’ve achieved so I could copy it for my purposes. Have you had a look at the Grocy integration? That’s what I’m using for storing the underlying chores and how often they need to repeat. It means you don’t need separate sensors for each chore - there’s just one for Grocy containing all of the chores as attributes.

Really like this idea, but looks for me for way too much work to get all I want into this :slight_smile: Will keep an eye out for later.

Thanks for your reply ! I’ve also started to use grocy, it’s very handy and its barcode capabilities allow for a few neat tricks.

I hadn’t thought of using grocy’s sensor as you suggested, but I’ll definitely try in the near future !

I’m very impressed by the screenshots. I’ve seen Grocy too, but it looks way to complex for my needs.
I’ve tried to add the chores.yaml, but I’m sure I understand those mqtt sensors:

  - state_topic: 'home/chores/chores_bedroom_clean_floor'
    name: chores_bedroom_clean_floor
    json_attributes_topic: 'home/chores/chores_bedroom_clean_floor'
    <<: *chore_sensor_mqtt_config

How are they supposed to be updated? The state is always “unknown”. I get this message in lovelace:

ButtonCardJSTemplateError: TypeError: Cannot read property 'state' of undefined in 'var tsSensor = parseInt(entity.state) * 1000; var daysSinceLastUpdate = Math.floor(Math.abs(new ...'

It looks like you’re having an error related to your button card. Keep in mind that, although you added chores.yaml, there’s also a particuliar configuration related to lovelace (see my initial post for details).

If I had to guess, from what you posted : the button card is looking for an entity that either doesn’t exist, or is not reachable for some kind of reason.

In my example, I’m using lovelace-gen (see link above) for templating. If you’re not familiar with it, or not using it in your configuration, this will result in errors. There’s extended documentation about how to use this very useful tool.

How are they supposed to be updated?

In my setup : short press/click on a chore button will update the due date. Long press will display the settings.

I’m really busy those days, so I can’t work on improving my code. moreover, i’ve switch to using grocy/barcodebuddy, which as you mentioned is dense but also very useful in my opinion.

Thanks for your reply.

The issue was that “chores” was never defined, so button-card did not find the given entity.
I got it working by including the lovelace chores.yaml (chores_item.yaml) like so… is this the same you had or do you have a better idea? Unfotunately I can’t find a way to make the chores-list dynamic. :frowning:

# lovelace_gen

{% set chores = ["chores_bedroom_clean_dust", "chores_bedroom_clean_floor"] %}
type: custom:vertical-stack-in-card
title: Chores
cards:
  {% for c in chores %}
  - !include
      - chores_item.yaml
      - chore: {{ c }}
    {% endfor %}

Ok so, bare with me because this one is wild.

Sidenote : I haven’t been working on my config for the last three month, so so of what follows may be outdated. I’m also not a dev as I mentioned, so this is not state of the art.

I’m using lovelace-gen to pass variables to the template. This is an extract from the code for my chore view (that should have been included at first, but I’m silly so i forgot) :

# lovelace_gen
# ------------------------------------------------------------------------------
# File: lovelace/views/active/chores.yaml
# Desc: Used by lovelace_gen to generate Lovelace ui.
#       This is the chores tab.
# ------------------------------------------------------------------------------
# View settings.
icon: 'mdi:broom'
id: 'chores'
panel: true
path: 'chores'
title: 'Corvées'
# ------------------------------------------------------------------------------
# Layout config
cards:
- layout: vertical
  max_columns: 4
  type: custom:layout-card
  # ------------------------------------------------------------------------------
  # Cards
  cards:
  # -----------------------------------------------------------------------------
  # Maintenance and care for rooms
  # Study-----------------------------------------------------------------------------------------------------------------------------------
  - type: custom:vertical-stack-in-card
    title: Bureau
    cards:
    - !include
      - ../04_elements/e_btn_chore.yaml
      - chore: chores_livingroom_clean_floor
        name: {{ 'Nettoyer Le Sol'|capitalize }}
    - !include
      - ../04_elements/e_btn_chore.yaml
      - chore: chores_livingroom_clean_dust
        name: Faire la poussière

The important part of this snippet revolves around the !include statement. The first element of the include refers to the template I’m using, while the other is a list of variables that will be passed as well.

In the first item :

- !include
      - ../04_elements/e_btn_chore.yaml
      - chore: chores_livingroom_clean_floor
        name: {{ 'Nettoyer Le Sol'|capitalize }}

When lovelace-gen “reads” the template, it’ll consider

entity: '{{ "sensor." ~ chore }}'

as

entity: sensor.chores_livingroom_clean_floor

As some may point out, this is kind of useless, and one could achieve the same result without overthinking the whole process. I do consider this quite fun though, so this is more a hobby than a practical solution, please consider that.

There’s a way to use array and for loops to shorten the code, but I wouldn’t recommend using this unless you have a huge amount of chores to manage.

I’m moving into my new place next month, so I’ll take the opportunity to refactor & improve my entire setup. I’ll share my config by then.

2 Likes

Very nice work, I like your approach, and thanks for giving me inspiration.
I did a version of this to fits in my needs. One thing I don’t like about your solution is the need to pre-configure sensors before deploying it in the lovelace, my approach is a little different and more like the well known check button card which creates the sensor needed in the lovelace.

ui-lovelace.yaml

- !include
  - ../templates/lovelace_gen/chore.yaml
  - name: Limpeza Máq. Lavar loiça
    sensor_name: chore_washdisher_cleaning
    warning_before: 10
    cycle_days: 90

lovelace_gen template

# lovelace_gen

{% set entity = 'sensor.'+sensor_name %}

type: 'custom:button-card'
name: {{name}}
entity: {{entity}}
label: >
  [[[ return variables.var.label ]]]
show_label: true
icon: >
  [[[ return variables.var.icon ]]]
custom_fields:
  status: >
    [[[ return '<span style="display: inline-block; color: white; background: '+variables.var.color+'; padding: 0 5px; border-radius: 5px;">'+variables.var.days_left+'</span>' ]]]
styles:
  grid:
    - grid-template-areas: '"i n status" "i l status"'
    - grid-template-columns: 15% 1fr 1fr
    - grid-template-rows: 1fr 1fr
  icon:
    - color: >
        [[[ return variables.var.color ]]]
  label:
    - color: var(--disabled-text-color)
    - justify-self: start
  name: 
    - justify-self: start
variables:
  var: >
    [[[
      let colors = {};
      colors["success"] = "#8BC24A";
      colors["warning"] = "#FFC107";
      colors["error"] = "#FF5252";
      colors["disabled"] = "var(--disabled-text-color)";
      
      let result = {};
      result.label = "Clique para criar";
      result.color = colors["disabled"];
      result.icon = "mdi:alert-plus";
      result.days_left = "";
      let timestamp;
      let time;
      let minutes;
      let hours;
      let days;
      
      if (states['{{entity}}']) {
        if (entity.state != 'unknown') {
          timestamp = parseInt(entity.state);
          time = (Date.now() / 1000) - timestamp;
          minutes = Math.floor(((time % 3600) / 60));
          hours = Math.floor(((time % 86400) / 3600));
          days = Math.floor((time / 86400));
          
          result.color = colors["success"];
          result.icon = "mdi:checkbox-marked-circle-outline";

          // LAST TRIGGER
          if (time < 60)
            result.label = 'menos de 1 minuto';
          else if (days == 1)
            result.label = '1 dia atrás';
          else if (days > 1)
            result.label = days+' dias atrás';
          else if (hours >= 1)
            result.label = hours+' horas atrás';
          else if (hours < 1)
            result.label = minutes+(minutes > 1 ? ' minutos atrás' : ' minuto atrás');
          
          // DAYS LEFT
          result.days_left = Math.round(((timestamp + ({{cycle_days|int}}*86400)) - (Date.now()/1000)) / 86400);
          if (result.days_left <= {{warning_before|int}}) {
            result.color = colors["warning"];
            result.icon = "mdi:clock-alert";
          }
          if (result.days_left <= 0) {
            result.color = colors["error"];
            result.icon = "mdi:alert-circle"
          }
          result.days_left = result.days_left + (result.days_left == 1 ? " dia restante" : " dias restantes");

        } else {
          result.label = "Clique para publicar";
        }
      }
      return result;
      
    ]]]
tap_action:
  confirmation:
    text: >
      [[[
        if (!states['{{entity}}'])
          return 'A entidade com o nome {{entity}} vai ser criada'
        else
          return 'Tem a certeza que quer marcar a tarefa como concluída?'
      ]]]
  action: call-service
  service: mqtt.publish
  service_data:
    topic: >
      [[[
        if (!states['{{entity}}'])
          return 'homeassistant/sensor/{{sensor_name}}/config'
        else
          return 'homeassistant/sensor/{{sensor_name}}/state'
      ]]]
    payload: >
      [[[
        if (!states['{{entity}}'])
          return '{ "name": "{{sensor_name}}", "state_topic": "homeassistant/sensor/{{sensor_name}}/state", "device_class": "timestamp" }'
        else
          return Date.now() / 1000
      ]]]
    retain: true
2 Likes

This looks amazing but seems so hard to setup, does anyone have like a little guide on how to set this up for a family of 2 ?

I wrote a little guide here