Hi guys,
I've been using HA for a few years, and now starting to get into more complex coding projects. I'm currently working on a chore tracker, and I'd be grateful for any feedback or tips you can give me.
(I have looked at add ons and HACS but nothing quite fit what I wanted, hence making it from scratch. This is loosely based on the Sweepy app, which I had previously been using but I want my own offline/free/tailored-to-me solution)
My issue: Currently card 2 is acting as one giant button, hence the workaround with browser_mod.
- It works fine but it's a bit clunky
- The browser_mod chores aren't dynamic and so aren't in the same order as they appear on card 2.
- It also means I have to manually update the tap_action and the hold_action menus every time I add/remove a chore
- I'm also thinking further down the line I would like to have card 1 in each room on my main dashboard, and clicking it would bring up card 2, so the less sequential popups the better.
I would like card 2 to be a dynamic list in due order (as it is currently) but each chore is its own button, where tap_action automatically updates the right input.datetime for that chore and hold_action automatically brings up the right input.datetime to edit the date. Would be really grateful for your thoughts on how to achieve that!
Also (minor detail though) if anyone can suggest how to remove the space between the last chore and the bottom of the card containing the chore list that would be fab.
Thanks!
Current setup (in a separate chore dashboard):
-
each room looks something like this. It's a vertical stack with card 1 [overview of the room cleanliness] and card 2 [all the chores specific to that room].
-
Card 1 changes colour depending on the state of chores in the room (due/overdue/ok/due soon), and says how many chores are due/overdue.
-
Card 2 is a dynamic list of all the chores in the room, ranked in due order. Colours change based on state (due/overdue/ok/due soon), and it says when it's next due / how long it's been overdue, and says when it was last done and how often it needs to be done.
-
a short press on Card 2 uses browser_mod to bring up the list of chores that I can click individually to say it's been done today
-
a long press on Card 2 uses browser_mod to bring up the chores' input.datetimes to manually adjust the date they were last done
Code
Code for card 1
type: custom:button-card
entity: sensor.groupe_salle_de_bain
name: Salle de bain
icon: mdi:shower
show_state: false
styles:
card:
- border-radius: 12px
- padding: 12px 16px
- margin-bottom: 4px
grid:
- grid-template-areas: "\"i n badge\""
- grid-template-columns: 40px 1fr auto
name:
- font-size: 16px
- font-weight: 500
- justify-self: start
icon:
- width: 24px
- color: var(--primary-text-color)
state:
- value: overdue
styles:
card:
- background: "#480C0C"
- value: due
styles:
card:
- background: "#4A2800"
- value: due_soon
styles:
card:
- background: "#3B3300"
- value: ok
styles:
card:
- background: "#1A3E29"
custom_fields:
badge: |
[[[
if (!entity || !entity.attributes) return '';
const overdue = entity.attributes.overdue_count;
const due = entity.attributes.due_count;
if (overdue > 0)
return `<span style="background:rgba(0,0,0,0.2);color:var(--primary-text-color);border-radius:12px;padding:2px 8px;font-size:12px">${overdue} en retard</span>`;
if (due > 0)
return `<span style="background:rgba(0,0,0,0.2);color:var(--primary-text-color);border-radius:12px;padding:2px 8px;font-size:12px">${due} Ă faire</span>`;
return `<span style="background:rgba(0,0,0,0.2);color:var(--primary-text-color);border-radius:12px;padding:2px 8px;font-size:12px">tout ok</span>`;
]]]
Code for card 2
type: custom:button-card
entity: sensor.groupe_salle_de_bain
show_icon: false
show_name: false
show_state: false
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Quelle(s) corvée(s) veux-tu mettre à jour?
right_button: Fini
right_button_appearance: filled
content:
type: vertical-stack
cards:
- type: custom:button-card
color_type: blank-card
- type: custom:button-card
name: Nettoyer l'évier
icon: mdi:faucet
layout: icon_name
aspect_ratio: 5/1
tap_action:
action: fire-dom-event
browser_mod:
service: script.chore_set_to_today
data:
target_entity: input_datetime.chore_last_done_bathroom_clean_sink
- type: custom:button-card
name: Nettoyer la douche
icon: mdi:shower
layout: icon_name
aspect_ratio: 5/1
tap_action:
action: fire-dom-event
browser_mod:
service: script.chore_set_to_today
data:
target_entity: input_datetime.chore_last_done_bathroom_clean_shower
- type: custom:button-card
name: Nettoyer les toilettes
icon: mdi:toilet
layout: icon_name
aspect_ratio: 5/1
tap_action:
action: fire-dom-event
browser_mod:
service: script.chore_set_to_today
data:
target_entity: input_datetime.chore_last_done_bathroom_clean_toilet
- type: custom:button-card
name: Nettoyer la bonde de la douche
icon: mdi:pipe
layout: icon_name
aspect_ratio: 5/1
tap_action:
action: fire-dom-event
browser_mod:
service: script.chore_set_to_today
data:
target_entity: input_datetime.chore_last_done_bathroom_clean_shower_plug
- type: custom:button-card
name: Nettoyer la fenĂȘtre de la salle de bain
icon: mdi:window-closed-variant
layout: icon_name
aspect_ratio: 5/1
tap_action:
action: fire-dom-event
browser_mod:
service: script.chore_set_to_today
data:
target_entity: input_datetime.chore_last_done_bathroom_window
- type: custom:button-card
color_type: blank-card
hold_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Quelle(s) corvée(s) veux-tu mettre à jour?
right_button: Fini
right_button_appearance: filled
content:
type: entities
entities:
- entity: input_datetime.chore_last_done_bathroom_clean_sink
name: Nettoyer l'évier
icon: mdi:faucet
- entity: input_datetime.chore_last_done_bathroom_clean_shower
name: Nettoyer la douche
icon: mdi:shower
- entity: input_datetime.chore_last_done_bathroom_clean_toilet
name: Nettoyer les toilettes
icon: mdi:toilet
- entity: input_datetime.chore_last_done_bathroom_clean_shower_plug
name: Nettoyer la bonde de la douche
icon: mdi:pipe
- entity: input_datetime.chore_last_done_bathroom_window
name: Nettoyer la fenĂȘtre de la salle de bain
icon: mdi:window-closed-variant
styles:
card:
- padding: 0
- background: none
- box-shadow: none
custom_fields:
chore_list:
- width: 100%
- text-align: left
grid:
- grid-template-areas: "\"chore_list\""
- grid-template-columns: 1fr
- align-items: start
custom_fields:
chore_list: |
[[[
if (!entity || !entity.attributes) return '';
const chores = typeof entity.attributes.chores === 'string'
? JSON.parse(entity.attributes.chores)
: entity.attributes.chores || [];
const colours = {
overdue: { bg: '#480C0C', text: 'var(--primary-text-color)' },
due: { bg: '#4A2800', text: 'var(--primary-text-color)' },
due_soon: { bg: '#3B3300', text: 'var(--primary-text-color)' },
ok: { bg: '#1A3E29', text: 'var(--primary-text-color)' }
};
const rows = chores.map(chore => {
const c = colours[chore.state] || colours.ok;
const formatDue = (days) => {
if (days < 0) {
const d = Math.abs(days);
if (d >= 365) {
const n = Math.floor(d / 365);
return `${n} an${n > 1 ? 's' : ''} de retard`;
} else if (d >= 30) {
const n = Math.floor(d / 30);
return `${n} mois de retard`;
} else if (d >= 7) {
const n = Math.floor(d / 7);
return `${n} semaine${n > 1 ? 's' : ''} de retard`;
} else {
return `${d} jour${d > 1 ? 's' : ''} de retard`;
}
} else if (days === 0) {
return `Ă faire aujourd'hui`;
} else {
if (days >= 365) {
const n = Math.floor(days / 365);
return `dans ${n} an${n > 1 ? 's' : ''}`;
} else if (days >= 30) {
const n = Math.floor(days / 30);
return `dans ${n} mois`;
} else if (days >= 7) {
const n = Math.floor(days / 7);
return `dans ${n} semaine${n > 1 ? 's' : ''}`;
} else {
return `dans ${days} jour${days > 1 ? 's' : ''}`;
}
}
};
const dueLabel = formatDue(chore.days_until_due);
const lastDone = chore.last_done && chore.last_done !== 'unknown'
? `fait le ${new Date(chore.last_done).toLocaleDateString('fr-FR', {day:'numeric', month:'long'})}`
: 'jamais fait';
const unitGender = (unitEnglish) => {
if (unitEnglish === 'weeks') {
return `toutes les`
} else if (unitEnglish === 'days' || unitEnglish === 'months') {
return `tous les`
} else {
return `bad unit`
}
};
const unitGrammar = unitGender(chore.unit);
const showFreqNumber = (freq) => {
if (freq > 1) {
return ` ${freq} `
} else {
return ` `
}
};
const dueFreqNumber = showFreqNumber(chore.frequency);
const unitFrench = (unitEnglish) => {
if (unitEnglish === 'days') {
return `jours`
} else if (unitEnglish === 'weeks') {
return `semaines`
} else if (unitEnglish === 'months') {
return `mois`
} else {
return `bad unit`
}
};
const dueFreqUnit = unitFrench(chore.unit);
const dueFrequency = `${unitGrammar}${dueFreqNumber}${dueFreqUnit}`;
return `
<div style="
display: flex;
align-items: center;
justify-content: space-between;
background: ${c.bg};
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 4px;
">
<div style="display:flex;align-items:center;gap:8px">
<ha-icon
icon="${chore.icon}"
style="color:${c.text};--mdc-icon-size:20px"
></ha-icon>
<div style="display:flex;flex-direction:column;gap:2px">
<span style="font-size:14px;font-weight:500;color:${c.text}">
${chore.name}
</span>
<span style="font-size:11px;color:${c.text};opacity:0.8">
${lastDone} - ${dueFrequency}
</span>
</div>
</div>
<span style="
font-size:12px;
font-weight:500;
color:${c.text};
background:rgba(0,0,0,0.2);
border-radius:10px;
padding:2px 8px;
">
${dueLabel}
</span>
</div>
`;
});
return `<div style="padding:0 0 8px">${rows.join('')}</div>`;
]]]
