Salut, voici une carte qui n’est pas réellement liée à ma maison, mais plutôt à mes activités pro et perso. J’avais un certain nombres d’outils pense-bête (agenda - tasks - keep chez Google, et Asana qui est un gestionnaire de projets, en gros des tâches mais qu’on peut affecter à quelqu’un et dont on peu gérer les échéances et les interactions). Une app pour chaque outil et les difficultés qui vont avec pour partager le contenu. J’ai tout centralisé dans HA avec une carte maison, construite avec button-card:
D’un seul coup d’œil je vois mes événements en cours ou à venir pour chaque calendrier, le nombre de tâches actives, si une tâche demande une attention particulière (le signe exclamation rouge)… Etc…
Toutes les icônes sur la gauche amènent à une sous vue qui détaille seulement les calendriers, c’est une carte HACS week-planner-card:
Les icônes de droite ouvrent un popup avec la carte HACS bubble-Card, qui contient une autre carte HACS, cette fois-ci flex-table-card.
Les données proviennent soit de Google Tasks:
Soit de Asana:
Je ne développerai pas ici car c’est une petite usine à gaz mais j’ai du créé des composants pour autoriser de vraies interactions avec ces deux services ( n’hésitez pas à MP si quelqu’un est intéressé par ça, je créerai un sujet dédié).
Pour finir, il y a une icône de météo pour le département d’à côté où je bosse régulièrement, qui évidemment permet de naviguer vers une subview plus développée.
Pour revenir à la carte de base, voici le code suivi des templates auquel il fait appel.
#//////////////////////////////////////////////////////////////////////////////////////#
#/////// //////////////#
#/////// CARD: Calendars
#/////// //////////////#
#//////////////////////////////////////////////////////////////////////////////////////#
type: custom:vertical-stack-in-card
# card_mod:
# class: stock # not stack
cards:
# ============================================================================== mushu
- type: custom:button-card
template:
- calendar_todo
variables:
name: Mushu
color: var(--fire)
bubble_path: "#mushu"
google_calendar: calendar.google_mushu
tplt_calendar: binary_sensor.tplt_calendar_mushu
todo_list: todo.google_mushu
# ========================================================================== gabrechal
- type: custom:button-card
template:
- calendar_todo
variables:
name: Gabrechal
color: var(--nova-color)
bubble_path: "#gabrechal"
google_calendar: calendar.google_gabrechal
tplt_calendar: binary_sensor.tplt_calendar_gabrechal
todo_list: todo.google_gabrechal
# ============================================================================== olive
- type: custom:button-card
template:
- calendar_todo
variables:
name: Olive
color: var(--yellow)
bubble_path: "#olive"
google_calendar: calendar.google_olive
tplt_calendar: binary_sensor.tplt_calendar_olive
todo_list: todo.google_olive
# ======================================================================== asana olive
- type: conditional
conditions:
- condition: user
users:
# - b51e228b56384578a8ac0b5b731b9c4b # nova user
# - daa087b7316e4133bdc52448c0d2dc75 # hometab user
- 5bb507e8c5f845028511086983cbabf5 # olive user
card:
type: custom:button-card
template:
- asana
variables:
title: Tâches
entity: binary_sensor.asana_tasks_perso_olive
color: var(--accent-color)
tasks_path: "#asana_perso"
create_path: "#asana_perso_create"
# =============================================================================== nova
# - type: conditional
# conditions:
# - condition: user
# users:
# - b51e228b56384578a8ac0b5b731b9c4b # nova user
# # - daa087b7316e4133bdc52448c0d2dc75 # hometab user
# # - 5bb507e8c5f845028511086983cbabf5 # olive user
# card:
# type: custom:button-card
# template:
# - asana
# variables:
# title: Nova
# entity: binary_sensor.asana_tasks_nova
- type: custom:button-card
template:
- calendar_todo
variables:
name: Nova
color: var(--water)
# bubble_path: "#nova"
bubble_path: "#it"
google_calendar: calendar.google_nova
tplt_calendar: binary_sensor.tplt_calendar_nova
# todo_list: todo.google_nova
todo_list: todo.google_it
# ========================================================================= asana nova
- type: conditional
conditions:
- condition: user
users:
- b51e228b56384578a8ac0b5b731b9c4b # nova user
# - daa087b7316e4133bdc52448c0d2dc75 # hometab user
# - 5bb507e8c5f845028511086983cbabf5 # olive user
card:
type: custom:button-card
template:
- asana
variables:
title: Tâches
entity: binary_sensor.asana_tasks_perso_nova
color: var(--accent-color)
tasks_path: "#asana_perso"
create_path: "#asana_perso_create"
# ===================================================================== asana mcorp
- type: custom:button-card
template:
- asana
variables:
title: Tâches mCorp
entity: binary_sensor.asana_tasks_marecorp
color: var(--sky)
tasks_path: "#asana_marecorp"
create_path: "#asana_marecorp_create"
styles: &third_button
custom_fields:
button3:
- left: calc( 100% - 124px + 16px) # calc( 100% - 152px) +style16px
custom_fields:
button3:
card:
<<: &meteo_marecorp
type: custom:button-card
tap_action:
action: navigate
navigation_path: "#meteo_marecorp"
entity: weather.30_sauveterre
template:
- icon
extra_styles: |
@keyframes wind {
0% { transform: translate(0px, 0px) rotate(0deg); }
15% { transform: translate(4px, -3px) rotate(5deg); }
30% { transform: translate(1px, 1px) rotate(-4deg); }
45% { transform: translate(6px, 3px) rotate(3deg); }
60% { transform: translate(-5px, -2px) rotate(-1deg); }
75% { transform: translate(4px, -4px) rotate(-5deg); }
100% { transform: translate(0px, 0px) rotate(0deg); }
}
styles:
card:
- <<: &meteo_card
- width: 68px # card48px + img_cell38px + gap4px
- height: 48px # card48px + img_cell38px + gap4px
- background: var(--secondary-background-color)
- border-radius: 50% 0 0 50%
- box-shadow: -8px 0px 16px var(--secondary-background-color) # mask text below
img_cell: &meteo_img_cell
- margin-left: -28px
icon: &meteo_icon
- color: |
[[[
if (states['binary_sensor.openmeteo_wind_30'].state === 'on') {
return 'var(--warning-color)' }
else {
return 'var(--accent-color)' }
]]]
- animation: |
[[[
if (states['binary_sensor.openmeteo_wind_30'].state === 'on') {
return 'wind 3s infinite' }
]]]
custom_fields: &meteo_custom_fields
temp:
- position: absolute
- left: 40px # img_cell38px + gap4px
- font-size: 14px
- color: var(--primary-text-color)
custom_fields:
temp:
card:
<<: *meteo_marecorp
template:
- value
state_display: "[[[ return Math.round(states['weather.30_sauveterre'].attributes.temperature) + '°' ]]]"
# =============================================================================== work
- type: custom:button-card
template:
- calendar_todo
variables:
name: Calendrier mCorp
color: var(--sky)
bubble_path: "#work"
google_calendar: calendar.google_mcorp
tplt_calendar: binary_sensor.tplt_calendar_work
todo_list: todo.google_work
# - spé Nova
- type: conditional
conditions:
- condition: user
users:
- b51e228b56384578a8ac0b5b731b9c4b # nova user
# - daa087b7316e4133bdc52448c0d2dc75 # hometab user
# - 5bb507e8c5f845028511086983cbabf5 # olive user
card:
type: custom:vertical-stack-in-card
# card_mod:
# class: stock
cards:
# ==================================================================== birthdays
- type: conditional
conditions:
- condition: state
entity: binary_sensor.tplt_calendar_birthdays
state: "on"
card:
type: custom:button-card
template:
- calendar
entity: calendar.google_birthdays
name: Evènements
icon: mdi:cake-variant
variables:
tplt_calendar: binary_sensor.tplt_calendar_birthdays
# ============================================================== epic free games
- type: conditional
conditions:
- condition: state
entity: calendar.epic_games_store_jeux_gratuits
state: "on"
card:
type: custom:button-card
template:
- full
entity: calendar.epic_games_store_jeux_gratuits
name: Jeu Gratuit Epic
styles:
icon:
- color: var(--primary-text-color)
state_display: "[[[ return entity.attributes.message; ]]]"
label: >
[[[
let end_time = entity.attributes.end_time;
let stamp = new Date(end_time);
return "Termine le " + stamp.getDate() + "/" + '0'+(stamp.getMonth()+1) + " à " + stamp.toLocaleTimeString().slice(0,-3)
]]]
icon_tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
initial_style: classic
adaptive: true
content:
type: markdown
card_mod:
# class: stock
style:
.: |
ha-markdown {
padding: 0px 0px 0px 0px !important;
}
ha-markdown $:
ha-markdown-element p ha-alert $: |
.issue-type::after {
border-radius: 8px !important;
}
content: |-
<ha-alert alert-type="info">Descriptif du jeu gratuit du moment:</ha-alert>
{{ state_attr('calendar.epic_games_store_jeux_gratuits','description') }}
right_button: "retour"
Les templates:
# ---------------------------------------------------------------- calendar solo
calendar:
template:
- full
name: "[[[ return variables.name ]]]"
# entity: "[[[ return variables.google_calendar ]]]"
entity: |
[[[
const cal = states[variables.google_calendar]
if (cal !== undefined)
return variables.google_calendar
else
return "sun.sun"
]]]
icon_tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.navigate
data:
path: /main-dashboard/agenda
# triggers_update:
# - "[[[ return variables.google_calendar ]]]"
# - "[[[ return variables.tplt_calendar ]]]"
state_display: "[[[ return states[variables.tplt_calendar].attributes.event_name ]]]"
label: "[[[ if (entity.state === 'on') return states[variables.tplt_calendar].attributes.current_event_end ; return states[variables.tplt_calendar].attributes.next_event_start ]]]"
styles:
img_cell:
- background-color: "[[[ if (entity.state === 'on') return 'var(--accent-color)' ]]]"
icon:
- color: "[[[ if (entity.state === 'on') return 'var(--accent-text-color)'; return 'var(--primary-text-color)' ]]]"
label:
- color: "[[[ if (entity.state === 'on') return 'var(--accent-color)'; return 'var(--secondary-text-color)' ]]]"
custom_fields:
bubble:
- color: "[[[ if (entity.state === 'on') return 'transparent' ]]]"
calendar_todo:
template:
- calendar
state_display: |
[[[
const tplt = states[variables.tplt_calendar];
if (tplt && tplt.state === 'on') {
return (tplt.attributes && tplt.attributes.event_name) ? tplt.attributes.event_name : 'Évènement (nom inconnu)';
}
return 'Aucun évènement';
]]]
label: |
[[[
const cal = states[variables.google_calendar];
const tplt = states[variables.tplt_calendar];
const todo = states[variables.todo_list];
if (cal && tplt && cal.state === 'on')
return (tplt.attributes.current_event_end) ? tplt.attributes.current_event_end : 'Fin inconnue';
if (tplt && tplt.state === 'on')
return (tplt.attributes.next_event_start) ? tplt.attributes.next_event_start : 'Début inconnu';
if (!todo || ['unknown', 'unavailable'].includes(todo.state))
return 'État inconnu';
const count = Number(todo.state);
if (isNaN(count)) return 'État inattendu';
switch (count) {
case 0: return 'Aucune tâche';
case 1: return '1 tâche';
default: return `${count} tâches`;
}
]]]
styles:
img_cell:
- background: |
[[[
const cal = states[variables.tplt_calendar];
if (!cal || !cal.attributes) return '';
const running = (cal.attributes.running ?? 0) + '%';
if (entity.state === 'on')
return 'radial-gradient('
+ variables.color + ','
+ variables.color + '55%, transparent 60%), '
+ 'conic-gradient('
+ variables.color + running
+ ' 0%, var(--primary-background-color) 0% 100%)';
return '';
]]]
icon:
- color: |
[[[
if (entity.state === 'on')
return 'var(--accent-text-color)';
const cal = states[variables.tplt_calendar];
if (cal && cal.attributes && cal.attributes.delay === 0)
return variables.color;
return '';
]]]
label:
- color: |
[[[
const cal = states[variables.tplt_calendar];
const todo = states[variables.todo_list];
const calOn = cal && cal.state === 'on';
const todoCount = todo ? Number(todo.state) : 0;
if (calOn || (!isNaN(todoCount) && todoCount > 0))
return variables.color;
return 'var(--secondary-text-color)';
]]]
custom_fields:
bubble:
- opacity: |
[[[
const cal = states[variables.tplt_calendar];
if (cal && cal.attributes && cal.attributes.delay === 1)
return '1';
return '0';
]]]
- color: |
[[[
return variables.color;
]]]
button1:
- display: |
[[[
const todo = states[variables.todo_list];
if (todo)
return '';
return 'none';
]]]
custom_fields:
button1:
card:
type: custom:button-card
template:
- icon
entity: "[[[ return variables.todo_list ]]]"
icon: mdi:clipboard-list
styles:
card:
- box-shadow: 0px 0px 12px 8px var(--secondary-background-color)
- background: var(--secondary-background-color)
img_cell:
- background: |
[[[
const todo = states[variables.todo_list];
const count = todo ? Number(todo.state) : 0;
if (!isNaN(count) && count === 0)
return 'var(--secondary-background-color)';
return variables.color;
]]]
icon:
- width: 24px
- height: 24px
- color: |
[[[
const todo = states[variables.todo_list];
const count = todo ? Number(todo.state) : 0;
if (!isNaN(count) && count !== 0)
return 'var(--accent-text-color)';
return 'var(--secondary-text-color)';
]]]
custom_fields:
notification:
- background-color: "[[[ return variables.color ]]]"
- opacity: |
[[[
const todo = states[variables.todo_list];
const count = todo ? Number(todo.state) : 0;
if (!isNaN(count) && count === 0)
return '0';
return '1';
]]]
custom_fields:
notification: |
[[[
const todo = states[variables.todo_list];
if (!todo) return "X";
const count = Number(todo.state);
if (!isNaN(count)) return count;
return "X";
]]]
tap_action:
action: |
[[[
const todo = states[variables.todo_list];
return todo ? "navigate" : "none";
]]]
navigation_path: |
[[[
const todo = states[variables.todo_list];
return todo ? variables.bubble_path : "";
]]]
# ------------------------------------------------------------------------ asana
asana:
template:
- calendar
entity: "[[[ return variables.entity ]]]"
name: >
[[[
const total = entity.attributes.all_count || 0;
const prefix = `<span style="font-weight: 600;">${variables.title}</span>`;
let suffix = ""
if (total > 0) {
suffix = `<span style="font-weight: 300;">${' • ' + total + ' au total'}</span>`;
}
return `${prefix + suffix}`;
]]]
state_display: |
[[[
const a = entity.attributes;
const conditions = [
{ test: a.today_count > 0, count: a.today_count, period: 'aujourd\'hui' },
{ test: a.tomorrow_count > 0, count: a.tomorrow_count, period: 'demain' },
{ test: a.week_count > 0, count: a.week_count, period: 'cette semaine' },
{ test: a.month_count > 0, count: a.month_count, period: 'ce mois-ci' },
{ test: a.other_count > 0, count: a.other_count, period: 'sans échéance' },
{ test: a.all_count === 0, message: 'Aucune tâche' }
];
const match = conditions.find(c => c.test);
if (!match) return '';
if (match.message) return match.message;
const plural = match.count === 1 ? ' tâche ' : ' tâches ';
return match.count + plural + match.period;
]]]
# label: |
# [[[
# const a = entity.attributes;
# const today = a.today_count || 0;
# const tomorrow = a.tomorrow_count || 0;
# const week = a.week_count || 0;
# const month = a.month_count || 0;
# const total = today > 0 ? tomorrow + week : month;
# const plural = total === 1 ? ' tâche' : ' tâches';
# return total === 0 ? 'Voir détail' : 'Dont ' + total + plural + ' cette semaine';
# ]]]
label: |
[[[
const today = entity.attributes.today_count || 0
const tomorrow = entity.attributes.tomorrow_count || 0
const week = entity.attributes.week_count || 0
const month = entity.attributes.month_count || 0
const other = entity.attributes.other_count || 0
if (today > 0) {
if (tomorrow > 0)
return 'Et ' + tomorrow + ' tâche' + (tomorrow > 1 ? 's' : '') + ' demain'
if (week > 0)
return 'Et ' + week + ' tâche' + (week > 1 ? 's' : '') + ' cette semaine'
if (month > 0)
return 'Et ' + month + ' tâche' + (month > 1 ? 's' : '') + ' à venir'
return 'Voir détail'
}
if (tomorrow > 0) {
if (week > 0)
return 'Et ' + week + ' tâche' + (week > 1 ? 's' : '') + ' cette semaine'
if (month > 0)
return 'Et ' + month + ' tâche' + (month > 1 ? 's' : '') + ' à venir'
return 'Voir détail'
}
if (week > 0) {
if (month > 0)
return 'Et ' + month + ' tâche' + (month > 1 ? 's' : '') + ' à venir'
return 'Voir détail'
}
if (other > 0)
return 'Voir détail'
return 'Well Done!'
]]]
styles:
img_cell:
- background: |
[[[
if (entity.state === 'on' )
return `${variables.color}`
else return 'transparent'
]]]
icon:
- color: |
[[[
if (entity.state === 'on' )
return 'var(--accent-text-color)'
]]]
state:
- color: |
[[[
const a = entity.attributes;
if (a.overdue == 'active' ) return 'var(--error-color)';
if (a.today_count > 0 ) return 'var(--nova-color)';
if (a.tomorrow_count > 0 ) return 'var(--info-color)';
return 'var(--primary-text-color)';
]]]
label:
- color: "[[[ if (entity.attributes.all_count > 0) return `${variables.color}`; return 'var(--secondary-text-color)' ]]]"
custom_fields:
button1:
card:
type: custom:button-card
template:
- icon
entity: binary_sensor.asana_tasks_nova
icon: mdi:dots-triangle
styles:
img_cell:
- background: |
[[[
if ((entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count) != 0)
return `${variables.color}`;
return 'var(--secondary-background-color)';
]]]
icon:
- width: 24px
- height: 24px
- color: |
[[[
if ((entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count) != 0)
return 'var(--accent-text-color)';
if ((entity.attributes.all_count ) != 0)
return `${variables.color}`;
return 'var(--secondary-text-color)';
]]]
custom_fields:
notification:
- color: |
[[[
if ((entity.attributes.today_count + entity.attributes.tomorrow_count ) === 0)
return 'var(--accent-text-color)';
return 'var(--primary-background-color)';
]]]
- background-color: |
[[[
if (entity.attributes.overdue == 'active' ) return 'var(--error-color)';
if (entity.attributes.today_count > 0) return 'var(--nova-color)';
if (entity.attributes.tomorrow_count > 0) return 'var(--info-color)';
return `${variables.color}`;
]]]
- opacity: ¬if_opa |
[[[
if ((entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count) === 0) return '0';
return '1';
]]]
severity:
- position: absolute
- top: -3px
- left: 28px
- opacity: *notif_opa
# - height: 16px
- width: 16px
custom_fields:
notification: |
[[[
const today = entity.attributes.today_count || 0
const tomorrow = entity.attributes.tomorrow_count || 0
const week = entity.attributes.week_count || 0
if (today > 0)
return today;
if (tomorrow > 0)
return tomorrow;
if (week > 0)
return week;
return '';
]]]
# notification: |
# [[[ return entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count || "" ]]]
severity: |
[[[ return (entity.attributes.today_severity) == 'Urgent' ? '<ha-icon icon="mdi:alert" style="color: var(--error-color)"></ha-icon>':
(entity.attributes.today_severity) == 'Important' ? '<ha-icon icon="mdi:star" style="color: var(--warning-color)"></ha-icon>':
(entity.attributes.today_severity) == 'Achats' ? '<ha-icon icon="mdi:cart" style="color: var(--info-color)"></ha-icon>':
(entity.attributes.today_severity) == 'Corvées' ? '<ha-icon icon="mdi:autorenew" style="color: var(--purple)"></ha-icon>':
'';
]]]
tap_action:
action: navigate
navigation_path: |
[[[
return (entity.attributes.all_count) == 0
? `${variables.create_path}`
: `${variables.tasks_path}`;
]]]
hold_action:
action: perform-action
perform_action: |
[[[
if (browser_mod?.browserID == "olivecell")
return "notify.mobile_app_olivecell";
else if (browser_mod?.browserID == "novacell")
return "notify.mobile_app_novacell";
else return null;
]]]
data:
message: "command_launch_app"
data:
package_name: "com.asana.app"
# ======================================================================================
# ======================================================================================
# ============================================================================== climate
# ---------------------------------------------------------------------- climate
climate:
template:
- full
label: "[[[ return 'Consigne: ' + (typeof entity.attributes.temperature === 'number' ? entity.attributes.temperature + '°C' : 'N/C') ]]]"
state_display: >
[[[
if (entity.state === 'heat')
return 'En service'
if (entity.state === 'auto')
return 'Automatique'
else
return 'Hors service'
]]]
styles:
img_cell:
- background-color: >
[[[
if (entity.state === 'heat')
{
if (entity.attributes.temperature > entity.attributes.current_temperature)
return 'var(--warning-color)'
else
return 'var(--info-color)'
}
if (entity.state === 'auto')
return 'var(--success-color)'
else
return 'var(--secondary-background-color)'
]]]
icon:
- color: "[[[ if (entity.state === 'heat' || entity.state === 'auto') return 'var(--accent-text-color)' ; return 'var(--secondary-text-color)' ]]]"



