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.
A confirmation message pops up (I’m trying to avoid accidental clicks) :
After confirmation, the chore is updated :
Settings
Each chore button grants access to the chore settings with an hold action (+ browser mod) :
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