[ConcoursDash] un template streamline adaptatif pour les cartes d'ESP multicapteurs

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 :smiley: )

:ring: 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.

image

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

:dna: Anatomie du Template : Les Custom Fields

Le template utilise des custom_fields qui réagissent intelligemment :

:magnifying_glass_tilted_left: Détails des fonctionnalités (Custom Fields)

:globe_with_meridians: WiFi / Ethernet

  • Si entity_wifi est 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.

:thermometer: Temp_Hum_Lux (Le Cycleur)

  • Mode Dynamique : Si un input_select est 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.

:counterclockwise_arrows_button: 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.

:police_car_light: 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.

:laptop: 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.

:performing_arts: 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 :slight_smile: 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 :sweat_smile: )

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

:magnifying_glass_tilted_left: 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).

:globe_with_meridians: 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).

:thermometer: 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

image

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

image

pas de variable du tout

      - type: custom:streamline-card
        template: espconcours2
        variables:

image

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

Peek 01-04-2026 19-09

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 :slight_smile:

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 :smiley: )

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.

7 « J'aime »

image
il a pas fondu le ESP ?
:sweat_smile:
perso, je trouve inutile ce genre de sensor.

j’ai moins de ESP, mais je passe pas mon temps a regarder les infos du moment que tout fonctionne.


Hello,

Je n’y passe pas mon temps non plus, mais en cas de soucis je peux analyser plus rapidement.

Oui certain esp ont chaud, mais je pense plus a des soucis de sonde qu’autre chose, notemment sur les esp poe, certains sont à 40°C et d’autres à 90°C… avec les mêmes config et les mêmes boitiers, je serais fixé cet été :smiley:

cdt

3 « J'aime »