Hello,
Pensé pour gérer une multitudes d’esp multicapteurs Multi-capteurs DIY : radar de présence, température, lumière et bien plus et les suivre sans générer 10 cartes pour gérer 10 esp.
Je suis heureux de vous présenter le streamline template de gestion et de surveillance d’esp ( et un peu de custom button card, sinon pas de plaisir
)
Un Template pour les gouverner tous…
Voici le « Template Unique », conçu pour centraliser le monitoring de tous vos modules ESP (ESP32, Ethernet ou WiFi) avec un affichage ultra-compact et dynamique. Sa force ? Il s’adapte aux variables que vous lui donnez.

Les Prérequis (Entités attendues)
Strealine card GitHub - brunosabot/streamline-card: Streamline your Lovelace configuration with with a card template system. · GitHub
Custom button card GitHub - custom-cards/button-card: ❇️ Lovelace button-card for home assistant · GitHub
un input select sur les données température, humidité et luxmètre.
Pour profiter pleinement de toutes les fonctions, votre ESP doit exposer les entités suivantes (via ESPHome) :
(Attention à ne pas multiplier les entrées text sensor, binary sensor ou sensor dans vos codes d’esp, je l’ai ai mis ici dans chaque entités pour vous repérer plus facilement).
Status :
binary_sensor:
- platform: status
name: "Status"
WiFi : (Optionnel si Ethernet)
sensor:
- platform: wifi_signal
name: "WiFi Signal Strength"
update_interval: 60s
Uptime :
text_sensor:
- platform: template
name: "Uptime (Jours, Heures et Minutes)"
lambda: |-
int seconds = id(uptime_sensor).state;
int days = seconds / 86400;
seconds = seconds % 86400;
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
return (days > 0 ? std::to_string(days) + " j " : "") +
(hours > 0 ? std::to_string(hours) + " h " : "") +
(minutes > 0 ? std::to_string(minutes) + " min" : "0 min");
update_interval: 60s
entity_category: "diagnostic"
Version : (Version d’ESPHome)
text_sensor:
- platform: version
name: "${name}_version"
Mémoire :
text_sensor:
- platform: template
id: esp_memory
icon: mdi:memory
name: ESP Free Memory
lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024;
unit_of_measurement: 'kB'
state_class: measurement
entity_category: "diagnostic"
Température Interne : sensor (Internal Temp de l’ESP)
sensor:
- platform: internal_temperature
name: "intern_temp"
Capteurs de données : Température, Humidité, Lux, Radar (à adapter selon vos besoins et vos capteurs , j’utilise des LD2410 DHT22 et BH1750).
binary_sensor:
- platform: ld2410
has_target:
name: Radar Target
id: radar_has_target
sensor:
- platform: dht
pin: 27 (à vérifier)
temperature:
name: "Temperature"
accuracy_decimals: 2
device_class: "temperature"
filters:
- offset: -1.0
humidity:
name: "Humidite"
accuracy_decimals: 2
device_class: "humidity"
update_interval: 20s
- platform: bh1750
name: "Lumiere"
device_class: "illuminance"
address: 0x23
update_interval: 60s
Anatomie du Template : Les Custom Fields
Le template utilise des custom_fields qui réagissent intelligemment :
Détails des fonctionnalités (Custom Fields)
WiFi / Ethernet
- Si
entity_wifiest fourni : Affiche le signal en dB avec un fond semi-transparent. La couleur change dynamiquement selon la force du signal (Vert/Orange/Rouge). - Si absent : Bascule automatiquement en mode Ethernet (icône câble), fond transparent, et affiche l’état On/Off de
entity_status.
Temp_Hum_Lux (Le Cycleur)
- Mode Dynamique : Si un
input_selectest lié, vous basculez entre Temp, Hum et Lux d’un simple clic. - Adaptation des unités : Affiche automatiquement °, %, lx ou même klx (pour les fortes luminosités > 1000 lx).
- Alertes visuelles : La couleur du texte change selon des seuils de confort ou d’alerte prédéfinis.
Status & Version
- Status : L’icône clignote en rouge dès que le module passe hors ligne.
- Version : Compare la version de l’ESP avec
update.esphome_update. Si une mise à jour est disponible, l’icône clignote pour vous avertir.
Radar (Présence)
- Affiche une icône d’alerte clignotante en rouge si un mouvement est détecté. Reste discrètement en vert le reste du temps.
Santé du Hardware
- Mémoire (Heap) : L’icône clignote si la mémoire vive devient critique (< 60 KB).
- Température Interne : Alerte visuelle (clignotement) au-dessus de 65°C pour prévenir les risques de surchauffe dans les boîtiers.
Un rendu caméléon
Le design de la carte (la grille) reste fixe pour garder un alignement parfait sur votre tableau de bord, mais le contenu s’adapte à votre configuration :
- Variable absente ? Le champ affiche un « N/A » propre et gris au lieu d’une erreur orange ou d’un bloc vide, préservant l’esthétique globale.
- Pas de capteur Lux ? Le système de navigation est intelligent : un double-tap sur la zone de température vous redirigera vers l’historique de température par défaut si le Lux n’est pas configuré.
« Un Template pour les surveiller tous, un Template pour les trouver, un Template pour les ramener tous et dans Home Assistant les lier. »
le code du template ( attention, forcément ça pique
peut sans doute encore être optimisé mais répond à mon besoin )
le code du template
streamline_templates:
espconcours2:
card:
type: custom:button-card
triggers_update:
- '[[input_select_temp_hum_lux]]'
- '[[entity_temp]]'
- '[[entity_hum]]'
- '[[entity_lux]]'
- '[[entity_radar]]'
- update.esphome_update
show_name: false
styles:
card:
- background: rgba(20, 20, 20, 0.85) !important
- padding: 0px
- border-radius: 15px !important
- border: 2px solid rgba(255, 255, 255, 0.3)
- height: 56px
grid:
- grid-template-areas: |
[[[
return '[[entity_lux]]' ? '"name wifi temp_hum_lux uptime intern_temp" "name status radar version mem"' : '"name wifi temp_hum_lux uptime intern_temp" "name status radar version mem"';
]]]
- grid-template-columns: 0.6fr 0.5fr 0.5fr 0.5fr 0.6fr
- grid-template-rows: auto auto
custom_fields:
name:
- justify-content: center
- display: flex
uptime:
- justify-content: center
- display: flex
temp_hum_lux:
- justify-content: center
- display: flex
wifi:
- justify-content: center
- display: flex
intern_temp:
- justify-content: center
- display: flex
version:
- justify-content: center
- display: flex
status:
- justify-content: center
- display: flex
radar:
- justify-content: center
- display: flex
mem:
- justify-content: center
- display: flex
custom_fields:
name: |
[[[
const n = '[[name]]';
// Si la variable contient encore "[[name]]", on affiche un vide ou un nom par défaut
const displayName = n.includes('name') ? "ESP" : n;
return `<div style="text-align:left; font-size:12px; font-weight:bold; padding:2px 8px; display:flex; align-items:center; white-space:nowrap;">${displayName}</div>`;
]]]
wifi:
card:
type: custom:button-card
entity: >-
[[[ return '[[entity_wifi]]'.includes('entity_wifi') ?
'[[entity_status]]' : '[[entity_wifi]]' ]]]
show_state: true
show_name: false
show_icon: true
layout: icon_state
size: 17px
tap_action:
action: more-info
icon: |
[[[
const noWifi = '[[entity_wifi]]'.includes('entity_wifi');
if (noWifi) {
const s = states['[[entity_status]]'];
return (s && s.state.toLowerCase() === 'off') ? "mdi:ethernet-off" : "mdi:ethernet";
}
return "mdi:wifi";
]]]
state_display: |
[[[
const noWifi = '[[entity_wifi]]'.includes('entity_wifi');
if (noWifi) {
const s = states['[[entity_status]]'];
if (!s || s.state === 'unavailable') return "N/A";
return (s.state.toLowerCase() === 'on') ? "On" : "Off";
} else {
const s = states['[[entity_wifi]]'];
if (!s || s.state === 'unavailable') return "N/A";
return Math.round(parseFloat(s.state)) + " dB";
}
]]]
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
styles:
card:
- display: |
[[[
const noWifi = '[[entity_wifi]]'.includes('entity_wifi');
const noStatus = '[[entity_status]]'.includes('entity_status');
return (noWifi && noStatus) ? "none" : "flex";
]]]
- box-shadow: none
- background: |
[[[
const noWifi = '[[entity_wifi]]'.includes('entity_wifi');
return noWifi ? "transparent" : "rgba(0,0,0,0.5)";
]]]
- border-radius: 10px
- padding: 2px
- cursor: pointer
- height: 20px
- min-height: 22px
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- position: relative !important
- margin-right: 4px !important
- color: |
[[[
const noWifi = '[[entity_wifi]]'.includes('entity_wifi');
if (noWifi) {
const s = states['[[entity_status]]'];
if (!s || s.state === 'unavailable') return "grey";
return (s.state.toLowerCase() === 'on') ? "green" : "red";
} else {
const s = states['[[entity_wifi]]'];
if (!s || s.state === 'unavailable') return "grey";
const value = parseInt(s.state);
return value <= -70 ? "red" : value <= -55 ? "orange" : "green";
}
]]]
state:
- font-size: 12px
- font-weight: bold
- white-space: nowrap
temp_hum_lux:
card:
type: custom:button-card
entity: |
[[[
const toggleEntity = '[[input_select_temp_hum_lux]]';
const hasToggle = toggleEntity && !toggleEntity.includes('[[');
return hasToggle ? toggleEntity : '[[entity_temp]]';
]]]
tap_action: |
[[[
const toggleEntity = '[[input_select_temp_hum_lux]]';
const hasToggle = toggleEntity && !toggleEntity.includes('[[');
if (hasToggle) {
return {
action: 'call-service',
service: 'input_select.select_next',
service_data: {
entity_id: toggleEntity
}
};
} else {
return {
action: 'more-info',
entity: '[[entity_temp]]'
};
}
]]]
double_tap_action: |
[[[
const toggleEntity = '[[input_select_temp_hum_lux]]';
const hasToggle = toggleEntity && !toggleEntity.includes('[[');
if (!hasToggle) {
return {
action: 'more-info',
entity: '[[entity_temp]]'
};
}
const toggle = states[toggleEntity];
if (!toggle) {
return {
action: 'more-info',
entity: '[[entity_temp]]'
};
}
const luxEntity = '[[entity_lux]]';
const hasLux = luxEntity && !luxEntity.includes('[[');
const humEntity = '[[entity_hum]]';
const hasHum = humEntity && !humEntity.includes('[[');
let targetEntity = '[[entity_temp]]';
switch(toggle.state) {
case 'Lumiere':
targetEntity = hasLux ? luxEntity : '[[entity_temp]]';
break;
case 'Humidite':
targetEntity = hasHum ? humEntity : '[[entity_temp]]';
break;
case 'Temperature':
default:
targetEntity = '[[entity_temp]]';
}
return {
action: 'more-info',
entity: targetEntity
};
]]]
show_icon: true
show_name: false
show_state: true
icon: |
[[[
const toggleEntity = '[[input_select_temp_hum_lux]]';
const hasToggle = toggleEntity && !toggleEntity.includes('[[');
if (!hasToggle) {
return "mdi:thermometer";
}
const toggle = states[toggleEntity];
if (!toggle) return "mdi:thermometer";
switch(toggle.state) {
case 'Lumiere':
return "mdi:brightness-5";
case 'Humidite':
return "mdi:water-percent";
case 'Temperature':
default:
return "mdi:thermometer";
}
]]]
layout: icon_state
state_display: |
[[[
const temp = states['[[entity_temp]]'];
const toggleEntity = '[[input_select_temp_hum_lux]]';
const hasToggle = toggleEntity && !toggleEntity.includes('[[');
// Si pas de toggle, afficher simplement la température
if (!hasToggle) {
if (!temp || temp.state === 'unavailable') return "N/A";
return parseFloat(temp.state).toFixed(1) + "°";
}
const toggle = states[toggleEntity];
const luxEntity = '[[entity_lux]]';
const hasLux = luxEntity && !luxEntity.includes('[[');
const lux = hasLux ? states[luxEntity] : null;
const humEntity = '[[entity_hum]]';
const hasHum = humEntity && !humEntity.includes('[[');
const humidity = hasHum ? states[humEntity] : null;
if (!temp || !toggle) return '-';
const tempValue = parseFloat(temp.state);
if (isNaN(tempValue)) return '-';
switch(toggle.state) {
case 'Lumiere':
if (hasLux && lux) {
const luxValue = parseFloat(lux.state);
if (isNaN(luxValue)) return '-';
// 🔹 Conversion en klx si > 999
if (luxValue >= 1000) {
return (luxValue / 1000).toFixed(1) + 'klx';
} else {
return Math.round(luxValue) + 'lx';
}
}
return tempValue.toFixed(1) + '°';
case 'Humidite':
if (hasHum && humidity) {
const humidityValue = parseFloat(humidity.state);
return isNaN(humidityValue) ? '-' : humidityValue.toFixed(0) + '%';
}
return tempValue.toFixed(1) + '°';
case 'Temperature':
default:
return tempValue.toFixed(1) + '°';
}
]]]
color: |
[[[
const toggleEntity = '[[input_select_temp_hum_lux]]';
const hasToggle = toggleEntity && !toggleEntity.includes('[[');
const temp = states['[[entity_temp]]'];
if (!temp || temp.state === 'unavailable') return "grey";
if (!hasToggle) {
const tempValue = parseFloat(temp.state);
if (tempValue <= 15) return "skyblue";
else if (tempValue <= 20) return "green";
else if (tempValue <= 25) return "orange";
else if (tempValue <= 29) return "red";
else return "brown";
}
const toggle = states[toggleEntity];
if (!toggle) return "grey";
const luxEntity = '[[entity_lux]]';
const hasLux = luxEntity && !luxEntity.includes('[[');
const lux = hasLux ? states[luxEntity] : null;
const humEntity = '[[entity_hum]]';
const hasHum = humEntity && !humEntity.includes('[[');
const humidity = hasHum ? states[humEntity] : null;
switch(toggle.state) {
case 'Lumiere':
if (hasLux && lux) {
const luxValue = parseInt(lux.state);
if (luxValue <= 50) return "grey";
else if (luxValue <= 250) return "orange";
else return "white";
}
break;
case 'Humidite':
if (hasHum && humidity) {
const humValue = parseFloat(humidity.state);
if (humValue <= 30) return "red";
else if (humValue <= 40) return "orange";
else if (humValue <= 60) return "green";
else if (humValue <= 70) return "blue";
else return "purple";
}
break;
}
// Par défaut, température
const tempValue = parseFloat(temp.state);
if (tempValue <= 15) return "skyblue";
else if (tempValue <= 20) return "green";
else if (tempValue <= 25) return "orange";
else if (tempValue <= 29) return "red";
else return "brown";
]]]
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 4px
- padding: 2px
- cursor: pointer
- height: 24px
- min-height: 24px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- position: relative !important
- margin-right: 4px !important
state:
- font-size: 12px
- font-weight: bold
uptime:
card:
type: custom:button-card
entity: '[[entity_uptime]]'
show_state: true
show_name: false
show_icon: true
layout: icon_state
tap_action:
action: more-info
icon: mdi:timer-outline
color: white
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
state_display: |
[[[
const s = states['[[entity_uptime]]'];
if (!s || s.state === 'unavailable') return "N/A";
// Remplace "min" par "m" pour gagner de l'espace
return s.state.replace(' min', ' m');
]]]
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 6px
- padding: 2px
- cursor: pointer
- height: 28px
- min-height: 28px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- position: relative !important
- margin-right: 4px !important
state:
- font-size: 12px
- font-weight: bold
intern_temp:
card:
type: custom:button-card
entity: '[[entity_intern_temp]]'
show_state: true
show_name: false
show_icon: true
layout: icon_state
tap_action:
action: more-info
icon: |
[[[
return "mdi:thermometer";
]]]
color: |
[[[
const s = states['[[entity_intern_temp]]'];
if (!s || s.state === 'unavailable') return "grey";
const value = parseFloat(s.state);
return value >= 65 ? "red" : value >= 50 ? "orange" : "green";
]]]
state_display: |
[[[
const s = states['[[entity_intern_temp]]'];
if (!s || s.state === 'unavailable') return "N/A";
return parseFloat(s.state).toFixed(1) + "°";
]]]
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 4px
- padding: 2px
- cursor: pointer
- height: 24px
- min-height: 24px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- position: relative !important
- margin-right: 4px !important
- animation: |
[[[
const s = states['[[entity_intern_temp]]'];
if (!s || s.state === 'unavailable') return "none";
const value = parseFloat(s.state);
return value >= 65 ? "blink 1s infinite" : "none";
]]]
state:
- font-size: 12px
- font-weight: bold
radar:
card:
type: custom:button-card
entity: '[[entity_radar]]'
show_state: true
show_name: false
show_icon: true
layout: icon_state
tap_action:
action: more-info
icon: |
[[[
const s = states['[[entity_radar]]'];
if (!s || s.state === 'unavailable') return "mdi:account";
return s.state === 'on' ? "mdi:account-alert" : "mdi:account";
]]]
state_display: |
[[[
const s = states['[[entity_radar]]'];
if (!s || s.state === 'unavailable') return "N/A";
return s.state === 'on' ? 'Alert' : 'Clear';
]]]
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 4px
- padding: 2px
- cursor: pointer
- height: 24px
- min-height: 24px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- position: relative !important
- margin-right: 4px !important
- color: |
[[[
const s = states['[[entity_radar]]'];
if (!s || s.state === 'unavailable') return "grey";
return s.state === 'on' ? "red" : "green";
]]]
- animation: |
[[[
const s = states['[[entity_radar]]'];
if (!s || s.state === 'unavailable' || s.state === 'off') return "none";
return "blink 1s infinite";
]]]
state:
- font-size: 12px
- font-weight: bold
version:
card:
type: custom:button-card
entity: '[[entity_version]]'
show_state: true
show_name: false
show_icon: true
layout: icon_state
tap_action:
action: more-info
icon: mdi:information-outline
color: white
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
state_display: |
[[[
const s = states['[[entity_version]]'];
if (!s || s.state === 'unavailable') return 'N/A';
// Extrait uniquement le numéro de version (ex: "2025.9.1")
const version = s.state.split(' ')[0];
return version;
]]]
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 6px
- padding: 2px
- cursor: pointer
- height: 28px
- min-height: 28px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- margin-right: 4px
- position: relative !important
- color: |
[[[
const installedOnThisEsp = states['[[entity_version]]']?.state.split(' ')[0];
const latestAvailable = states['update.esphome_update']?.attributes?.latest_version?.split(' ')[0];
if (!installedOnThisEsp || !latestAvailable) return 'white';
if (installedOnThisEsp !== latestAvailable) return 'red';
return 'green';
]]]
- animation: |
[[[
const installedOnThisEsp = states['[[entity_version]]']?.state.split(' ')[0];
const latestAvailable = states['update.esphome_update']?.attributes?.latest_version?.split(' ')[0];
if (installedOnThisEsp && latestAvailable && installedOnThisEsp !== latestAvailable) {
return 'blink 1s infinite';
}
return 'none';
]]]
state:
- font-size: 12px
- font-weight: bold
status:
card:
type: custom:button-card
entity: '[[entity_status]]'
show_state: true
show_name: false
show_icon: true
layout: icon_state
tap_action:
action: more-info
icon: |
[[[
const s = states['[[entity_status]]'];
if (!s || s.state === 'unavailable') return "mdi:help-network-outline";
return s.state === 'on' ? "mdi:check-network-outline" : "mdi:close-network-outline";
]]]
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
state_display: |
[[[
const s = states['[[entity_status]]'];
if (!s || s.state === 'unavailable') return "N/A";
return s.state === 'on' ? 'On' : 'Off';
]]]
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 4px
- padding: 1px 2px
- cursor: pointer
- height: 24px
- min-height: 24px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- margin-right: 4px
- position: relative !important
- color: |
[[[
const s = states['[[entity_status]]'];
if (!s || s.state === 'unavailable') return "orange";
return s.state === 'on' ? "green" : "red";
]]]
- animation: |
[[[
const s = states['[[entity_status]]'];
if (!s || s.state === 'unavailable' || s.state === 'off') return "blink 1s infinite";
return "none";
]]]
state:
- font-size: 12px
- font-weight: bold
mem:
card:
type: custom:button-card
entity: '[[entity_mem]]'
show_state: true
show_name: false
show_icon: true
layout: icon_state
tap_action:
action: more-info
icon: mdi:memory
color: |
[[[
const s = states['[[entity_mem]]'];
if (!s || s.state === 'unavailable') return "grey";
const value = parseInt(s.state);
return value < 60 ? "red" : value < 160 ? "orange" : "green";
]]]
state_display: |
[[[
const s = states['[[entity_mem]]'];
if (!s || s.state === 'unavailable') return "N/A";
return s.state + " KB";
]]]
size: 17px
extra_styles: |
#container {
grid-template-columns: min-content min-content !important;
width: fit-content !important;
}
styles:
card:
- box-shadow: none
- background: transparent
- border-radius: 4px
- padding: 2px
- cursor: pointer
- height: 24px
- min-height: 24px
- display: flex
- align-items: center
- justify-content: flex-start
- width: fit-content !important
icon:
- width: 17px
- height: 17px
- position: relative !important
- margin-right: 4px !important
- animation: |
[[[
const s = states['[[entity_mem]]'];
if (!s || s.state === 'unavailable') return "none";
const value = parseInt(s.state);
return value < 60 ? "blink 1s infinite" : "none";
]]]
state:
- font-size: 12px
- font-weight: bold
Comment je l’utilise, dashboard en mode éditeur de configuration
On colle le template tout en haut
l’identation plus bas, qd le dashboard commence vraiment ( oui j’ai pas mal de templates qui tournent
)
et ensuite dans le dashboard en édition classique, on appelle le template
Je suis en picture element, j’ai donc des style de positionnement pour les cartes
Zoom sur la logique
L’intelligence des Custom Fields
La force de ce template réside dans sa capacité à détecter si une variable a été remplie dans la carte streamline-card ou si elle est restée à sa valeur par défaut (le nom de la variable entre crochets).
Le bloc de Connexion (WiFi / Ethernet)
Logique : Le template vérifie si [[entity_wifi]] contient le texte "entity_wifi".
Si OUI (Absent) : Il bascule sur l'entité [[entity_status]]. L'icône devient un câble Ethernet. Si entity_status est aussi absent, le bloc s'auto-dissimule (display: none).
Si NON (Présent) : Il affiche le RSSI en dB. La couleur passe du vert au rouge selon la force du signal (seuil à -70dB et -55dB).
Le Cycleur (Temp / Hum / Lux)
C’est le champ le plus complexe car il gère trois types de données dans un seul espace :
Entité dynamique : Si [[input_select_temp_hum_lux]] est défini, la carte "écoute" cet input_select pour savoir quoi afficher. Sinon, elle reste fixée sur la température.
Interactivité : * Simple clic : Passe à l'affichage suivant (Temp -> Hum -> Lux).
Double clic : Ouvre la fenêtre "More-info" de l'entité actuellement affichée à l'écran.
Intelligence des unités : Le code détecte si la valeur dépasse 1000 lux pour basculer automatiquement l'unité en klx et arrondir la valeur, évitant que le texte ne dépasse de la carte.
Et suivant comment on appelle le template, on a un rendu différent
Le Radar (Présence)
Comportement : Il ne se contente pas d'afficher un état. Si l'entité passe à on, l'icône bascule sur mdi:account-alert et déclenche une animation de clignotement (blink).
Fallback : Si la variable [[entity_radar]] n'est pas fournie, il affiche un "N/A" discret pour ne pas casser la grille visuelle.
Gestion des Mises à Jour (Version)
Comparaison intelligente : Le template ne regarde pas juste la version. Il compare la chaîne de caractères de [[entity_version]] avec l'attribut latest_version de l'entité globale update.esphome_update.
Alerte visuelle : Si les deux versions diffèrent, l'icône passe au rouge et clignote. C'est l'indicateur parfait pour savoir quel ESP doit être flashé sans ouvrir le tableau de bord ESPHome.
Diagnostics Hardware (Mem & Intern_Temp)
Mémoire (Heap) : Affiche la mémoire vive disponible en KB. En dessous de 60 KB, l'ESP commence à être instable : le template vous prévient en faisant clignoter l'icône.
Température Interne : Crucial pour les ESP installés dans des cloisons ou des boîtiers fermés. Le seuil d'alerte est fixé à 65°C.
Pourquoi utiliser streamline-card ici ?
Le template utilise des variables de type [[ma_variable]]. L’avantage de passer par streamline-card est triple :
Réduction du code : Vous ne définissez vos 10 capteurs qu'une seule fois.
Maintenance simplifiée : Si vous voulez changer la couleur du "On" ou la vitesse du clignotement, vous le faites à un seul endroit pour tous vos ESP.
Éviter les erreurs : Si vous oubliez une entité (ex: pas de radar sur un modèle), le JavaScript du template gère l'erreur gracieusement sans faire planter l'interface.
- type: custom:streamline-card
template: espconcours2
variables:
- name: Entree1
- entity_wifi: sensor.esp1_entree_wifi_signal_strength
- entity_temp: sensor.esp1_entree_temperature1
- entity_uptime: sensor.esp1_entree_uptime_jours_heures_et_minutes
- entity_intern_temp: sensor.esp1_entree_intern_temp
- entity_status: binary_sensor.esp1_entree_status
- entity_lux: sensor.esp1_entree_lumiere1
- entity_radar: binary_sensor.esp1_entree_radar_target
- entity_version: sensor.esp1_entree_esp1_entree_version
- entity_mem: sensor.esp1_entree_esp_free_memory
- input_select_temp_hum_lux: input_select.toggle_temp_hum_lux_esp1_entree
- entity_hum: sensor.esp1_entree_humidite1

pas de variable wifi, il passe en ethernet
- type: custom:streamline-card
template: espconcours2
variables:
- name: Entree1
- entity_temp: sensor.esp1_entree_temperature1
- entity_uptime: sensor.esp1_entree_uptime_jours_heures_et_minutes
- entity_intern_temp: sensor.esp1_entree_intern_temp
- entity_status: binary_sensor.esp1_entree_status
- entity_lux: sensor.esp1_entree_lumiere1
- entity_radar: binary_sensor.esp1_entree_radar_target
- entity_version: sensor.esp1_entree_esp1_entree_version
- entity_mem: sensor.esp1_entree_esp_free_memory
- input_select_temp_hum_lux: input_select.toggle_temp_hum_lux_esp1_entree
- entity_hum: sensor.esp1_entree_humidite1

pas de variable du tout
- type: custom:streamline-card
template: espconcours2
variables:

tap action on bascule l’input select double tap on ouvre la carte more info

et c’est ainsi qu’avec un template de certe 700 lignes, on gère une armée d’esp avec quelques lignes d’appel pour chaque cartes. un simple calcul suffit, j’utilise 69 fois le template ( 1 x 700 + (69 x 15 lignes pour prendre large ) = 1700 lignes de code.
69 x 700 = 48300 lignes de code, à vous de voir ![]()
J’utilise en picture element, mais normalement ça pose pas de problème de l’utiliser autrement ( testé que en maçonnerie ).
Voilà, en résumé si vous utilisez ce type d’esp multicapteur, vous allez adorer ce template ( ou pas
)
Edit: petite précision, tout changement sur le template devra automatiquement être suivi d’un F5 dans le dashboard d’affichage, pour valider les changements visuellement.









