Hello,
Quand on commence à avoir pas mal de modules ESPHome à la maison, cliquer sur "Mettre à jour" sur chaque appareil peut vite devenir fastidieux. Et lancer un "Tout mettre à jour" simultanément peut parfois mettre à genoux le serveur ou saturer le réseau Wi-Fi lors des reconnexions simultanées.
Pour résoudre ça, je vous partage mon système de mise à jour en cascade (un par un) entièrement automatisé, accompagné d'une carte dynamique de type "centre de contrôle".
Les photos ont étés faites sur plusieurs versions de carte et d'automation ), donc il peut y avoir des incohérence ( suivant les versions, la v11 étant la dernière )
Ce que ça fait globalement :
Intelligence : L'automatisation détecte toute seule les modules ESPHome qui ont une mise à jour disponible et les trie par ordre alphabétique.
Séquentiel sécurisé : Elle lance la mise à jour du premier module, attend 1 minute, calcule le temps que l'opération a pris, puis passe au suivant.
Visuel : La carte affiche une barre de progression en temps réel (pourcentage, étape X/Y) et réorganise dynamiquement un tableau : l'ESP en cours de flash remonte automatiquement tout en haut (
), ceux en attente restent au milieu (
), et ceux terminés basculent en bas avec leur statut (
ou
) accompagné de la durée précise du flash.
Contrôles intégrés : Un clic lance la boucle, un appui long stoppe tout en cas de pépin, et un double-tap nettoie le rapport.
1. Les prérequis (Les Entrées / Inputs), entités firmware ou micrologiciel
Avant de commencer, vous devez créer deux entrées de type texte (Paramètres > Appareils et services > Entrées > Créer une entrée > Texte) :
input_text.esphome_maj_status (Sert à stocker le statut de la session sous forme de chaîne découpée).
input_text.esphome_maj_durees (Sert de dictionnaire pour lier chaque ESP à son temps de flash sous la forme entity:temps).
Il faut impérativement que les entités firmware ou micrologiciel soient activées dans chaque esp
- variables:
tous_les_esps: |-
{{ states.update
| selectattr('state', 'eq', 'on')
| selectattr('entity_id', 'search', '(_firmware|_micrologiciel)')
| map(attribute='entity_id') | list | sort }}
total_esp: "{{ tous_les_esps | count }}"
2. L'Automatisation
Voici le code de l'automatisation. Elle gère la réinitialisation, le filtrage des entités et le calcul précis du temps écoulé.
Code de l'automation
alias: "ESPHome : Maj en cascade"
description: "Mises à jour en cascade avec stockage du statut et des durées"
triggers: []
actions:
- action: input_text.set_value
data:
entity_id: input_text.esphome_maj_status
value: "idle|0|0|"
- action: input_text.set_value
data:
entity_id: input_text.esphome_maj_durees
value: ""
- variables:
tous_les_esps: |-
{{ states.update
| selectattr('state', 'eq', 'on')
| selectattr('entity_id', 'search', '(_firmware|_micrologiciel)')
| map(attribute='entity_id') | list | sort }}
total_esp: "{{ tous_les_esps | count }}"
- action: input_text.set_value
data:
entity_id: input_text.esphome_maj_status
value: "running|0|{{ total_esp }}|"
- repeat:
for_each: "{{ tous_les_esps }}"
sequence:
- action: input_text.set_value
data:
entity_id: input_text.esphome_maj_status
value: "running|{{ repeat.index }}|{{ total_esp }}|{{ repeat.item }}|{{ now().timestamp() | int }}"
- action: update.install
target:
entity_id: "{{ repeat.item }}"
- delay:
minutes: 1
- action: input_text.set_value
data:
entity_id: input_text.esphome_maj_durees
value: >-
{% set debut = states('input_text.esphome_maj_status').split('|')[4] | int %}
{% set duree = [0, (now().timestamp() | int - debut - 60)] | max %}
{% set actuel = states('input_text.esphome_maj_durees') %}
{{ (actuel ~ ',' if actuel | length > 0 else '') ~ repeat.item ~ ':' ~ duree }}
- action: input_text.set_value
data:
entity_id: input_text.esphome_maj_status
value: "done|{{ total_esp }}|{{ total_esp }}|"
mode: single
3. La Carte
Pour l'affichage, il vous faudra la célèbre carte personnalisée button-card (disponible sur HACS). Le code JavaScript intégré s'occupe de parser les données et de trier la liste en direct.
Code de la carte
type: custom:button-card
entity: input_text.esphome_maj_status
triggers_update: all
name: Mises à jour ESPHome en cascade
icon: mdi:cpu-64-bit
tap_action:
action: call-service
service: automation.trigger
service_data:
entity_id: automation.esphome_maj_en_cascade
hold_action:
action: call-service
service: automation.turn_off
service_data:
entity_id: automation.esphome_maj_en_cascade
double_tap_action:
action: call-service
service: input_text.set_value
service_data:
entity_id: input_text.esphome_maj_status
value: idle|0|0|
styles:
card:
- padding: 16px
- cursor: pointer
grid:
- grid-template-areas: "\"i n\" \"i subtitle\" \"actions actions\" \"table table\""
- grid-template-columns: auto 1fr
name:
- justify-self: start
- font-weight: bold
- font-size: 16px
icon:
- width: 28px
- grid-row: span 2
custom_fields:
subtitle:
- justify-self: start
- font-size: 12px
- color: window-list
- margin-top: 2px
actions:
- justify-self: start
- width: 100%
- margin-top: 6px
- margin-bottom: 4px
table:
- justify-self: start
- width: 100%
- margin-top: 12px
state:
- operator: template
value_template: |
[[[
const val = states['input_text.esphome_maj_status']?.state || '';
return val.startsWith('running');
]]]
styles:
icon:
- color: orange
- animation: blink 2s linear infinite
name:
- color: orange
custom_fields:
subtitle: |
[[[
const allEspsOn = Object.keys(states).filter(key =>
key.startsWith('update.') &&
(key.endsWith('_firmware') || key.endsWith('_micrologiciel')) &&
states[key].state === 'on'
).length;
return `Modules à mettre à jour : <span style="font-weight:bold;color:var(--primary-color);">${allEspsOn}</span>`;
]]]
actions: |
[[[
return `<div style="color:#555;line-height:2;">
<span style="font-size:14px;">👆 Tap : lancer les majs | ✋ Hold : stopper les majs</span><br>
<span style="font-size:14px;">👆👆 Double tap : effacer le rapport</span>
</div>`;
]]]
table: |
[[[
const val = states['input_text.esphome_maj_status']?.state || '';
const parts = val.split('|');
const status = parts[0] || '';
const idx = parseInt(parts[1]) || 0;
const total = parseInt(parts[2]) || 0;
const enCours = parts[3] || '';
const dureesRaw = states['input_text.esphome_maj_durees']?.state || '';
const dureesMap = {};
if (dureesRaw && dureesRaw !== 'unknown') {
dureesRaw.split(',').forEach(function(entry) {
const p = entry.split(':');
if (p.length === 2) dureesMap[p[0].trim()] = parseInt(p[1]) || 0;
});
}
const esps = Object.keys(states).filter(key =>
key.startsWith('update.') &&
(key.endsWith('_firmware') || key.endsWith('_micrologiciel')) &&
states[key].attributes.installed_version !== undefined &&
states[key].attributes.latest_version !== undefined &&
(states[key].attributes.release_url || '').includes('esphome')
).sort();
const aFaire = esps.filter(k => states[k].state === 'on');
if (status === '' || status === 'idle') {
if (aFaire.length === 0) {
return `<div style="color:grey;font-style:italic;padding-top:8px;">Aucune mise à jour disponible.</div>`;
}
return `<div style="color:grey;font-style:italic;padding-top:8px;">Cliquez pour lancer. Appui long pour stopper.</div>`;
}
const enCoursListe = enCours && states[enCours]?.state === 'on' ? [enCours] : [];
const aFaireListe = esps.filter(k => states[k].state === 'on' && k !== enCours);
const faitListe = esps.filter(k => states[k].state === 'off');
const listeAffichee = [...enCoursListe, ...aFaireListe, ...faitListe];
let headerBar = '';
if (status === 'running' || status === 'done') {
const pct = total > 0 ? ((idx / total) * 100).toFixed(1) : '0.0';
const barColor = status === 'done' ? '#4caf50' : 'orange';
const txtStatus = status === 'done' ? '✅ Terminé' : '🔄 En cours';
headerBar = `
<div style="padding:4px 0 12px 0;">
<div style="display:flex;justify-content:space-between;font-size:12px;color:#aaa;margin-bottom:4px;">
<span>${txtStatus} (${idx}/${total})</span>
<span style="color:${barColor};font-weight:bold;">${pct}%</span>
</div>
<div style="width:100%;background:#333;height:8px;border-radius:4px;overflow:hidden;">
<div style="width:${pct}%;background:${barColor};height:100%;border-radius:4px;transition:width 0.5s;"></div>
</div>
</div>`;
}
let rows = '';
listeAffichee.forEach(function(key) {
const attr = states[key].attributes;
const nom = (attr.friendly_name || key)
.replace(' Micrologiciel', '')
.replace(' firmware', '');
const etat = states[key].state;
const depart = attr.installed_version || '?';
const arrivee = attr.latest_version || '?';
const isInProgress = status === 'running' && enCours === key;
const isDone = etat === 'off';
const isSuccess = isDone && depart === arrivee;
const isFailed = isDone && depart !== arrivee;
let dureeStr = '';
if (isDone) {
const d = dureesMap[key] || 0;
if (d > 0) {
dureeStr = d >= 60
? `${Math.floor(d/60)}m${d%60}s`
: `${d}s`;
}
}
const statutDisplay = isInProgress ? '🔄' : isSuccess ? '✅' : isFailed ? '❌' : '⏳';
const color = isInProgress ? 'orange' : isSuccess ? '#4caf50' : isFailed ? '#f44336' : '#444';
const nameColor = isInProgress ? 'orange' : isSuccess ? '#4caf50' : isFailed ? '#f44336' : '#fff';
const bg = isInProgress ? 'rgba(255,165,0,0.06)' : 'transparent';
rows += `
<tr style="border-bottom:1px solid #1a1a1a;background:${bg};">
<td style="padding:6px 0;font-weight:500;color:${nameColor};">${nom}</td>
<td style="color:${color};font-size:14px;text-align:center;">${statutDisplay}</td>
<td style="color:#666;font-size:11px;text-align:center;">${depart}</td>
<td style="color:#aaa;font-size:11px;text-align:center;">${isDone ? arrivee : '→ ' + arrivee}</td>
<td style="color:#555;font-size:11px;text-align:center;">${dureeStr}</td>
</tr>`;
});
return `
<div>
${headerBar}
<table style="width:100%;border-collapse:collapse;text-align:left;font-size:13px;">
<thead>
<tr style="border-bottom:2px solid #333;color:#555;font-size:11px;text-transform:uppercase;">
<th style="padding:4px 0;">ESP</th>
<th style="text-align:center;width:40px;">État</th>
<th style="text-align:center;">Départ</th>
<th style="text-align:center;">Arrivée</th>
<th style="text-align:center;">Durée</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>`;
]]]

On lance, on fait autre chose et on va voir au besoin de temps en temps ou ça en est ![]()
Si vous testez et constatez un bug, remontez à la suite ![]()
cdt









