[ConcoursDash] Hyperviseur SCADA Résidentiel : Pilotage 4000Wc & EDF Tempo ☀️

Salut la communauté ! :waving_hand:

Moi c’est Teddy, technicien de maintenance en robotique industrielle. Pour ce concours, j’ai décidé de sortir de la domotique « gadget » pour entrer dans l’ère de la supervision SCADA. Mon objectif ? Transformer ma maison en une véritable petite usine intelligente où chaque watt est traqué, analysé et optimisé ! :factory::gem_stone:


:brain: LE CONCEPT : L’IHM (Interface Homme-Machine) Haute Performance

Pas de superflu, ici on parle d’aide à la décision. Mon dashboard est structuré selon une règle d’or industrielle : la sectorisation fonctionnelle en grille 25/50/25. :bar_chart:

  • :left_arrow: Zone 1 : Pilotage Réseau & Finances. Gestion stricte du contrat EDF Tempo. On ne subit plus les jours rouges, on les anticipe ! :euro_banknote:
  • :record_button: Zone 2 : Synoptique de Flux. La vision globale 360° des flux énergétiques en temps réel. :high_voltage:
  • :right_arrow: Zone 3 : Automatismes & Sécurité. L’intelligence active qui pilote la maison. :robot::shield:

:camera_with_flash: Aperçu


:high_voltage: SPÉCIFICATIONS TECHNIQUES (Le « Hard » :hammer_and_wrench:)

Pour faire tourner cette bête, l’infrastructure est solide :

  • :sun: Solaire : 4000 Wc de puissance installée (8 panneaux bifaciaux).
  • :battery: Stockage : 2x Anker Solarbank 3 E2700 Pro (Total 5,4 kWh).
  • :counterclockwise_arrows_button: Zéro Injection : Pilotage fin via Home Assistant pour un délestage dynamique.
  • :pager: Sonde : Smartmeter intégré pour une lecture instantanée au watt près.
  • :thermometer: Thermique : Gestion multi-zones avec remontée hybride (Température + Hygrométrie) sur chaque tuile.

:fire: LES « KILLER FEATURES » (Ce qui fait la différence :trophy:)

:green_circle: 1. L’Algorithme « Feu Vert » Électroménager :horizontal_traffic_light: C’est le cerveau du dashboard ! Basé sur une logique conditionnelle stricte :

  • SI SOC Batterie > 90% ET Production Solaire >= 2000W
  • ALORS les boutons Lave-Linge / Sèche-Linge passent au VERT et une notification Push est envoyée sur nos deux smartphones ! :mobile_phone::rocket:
  • Résultat : Ma femme et moi savons exactement QUAND consommer gratuitement sans jamais ouvrir une application complexe.

:chart_increasing_with_yen: 2. Calculateur de Coûts Tempo en Temps Réel :money_bag: Oubliez les estimations vagues. Mon système calcule dynamiquement le coût de la journée, du mois et de l’année en croisant les index HP/HC avec les couleurs Tempo du jour. C’est précis au centime près !

:mobile_phone: 3. UI Adaptive & Mobile-First (Responsive 2.0) :counterclockwise_arrows_button: En tant que tech, je suis souvent en mouvement. J’ai injecté des Media Queries CSS personnalisées pour que le dashboard s’adapte parfaitement :

  • Sur PC : Une grille large et imposante pour la salle de contrôle.
  • Sur Mobile : Une réorganisation automatique en colonne unique, avec suppression des hauteurs fixes pour une ergonomie tactile parfaite. Pas de scroll horizontal, pas de texte coupé ! :triangular_ruler::sparkles:

:thermometer: 4. Feedback Visuel de Chauffe :orange_circle: Chaque tuile de température est vivante. Si un radiateur ou le sèche-serviette demande de la puissance, la tuile s’illumine en orange. On voit la consommation avant même de consulter le compteur !


:hammer_and_wrench: STACK LOGICIELLE (HACS Power)

Pour les curieux, voici les briques utilisées :

  • layout-card (Pour la structure chirurgicale)
  • button-card (Pour le design 100% custom et le JS embarqué)
  • power-flow-card-plus (Pour l’animation des flux)
  • apexcharts-card (Pour l’analyse de performance)

:page_facing_up: Le Code (YAML) Voici le code complet du Dashboard. Il est « plug and play » si vous adaptez les entités.

type: custom:layout-card
layout_type: custom:grid-layout
layout:
  grid-template-columns: 28% 44% 28%
  grid-template-rows: auto
  grid-template-areas: |
    "gauche centre droite"
  mediaquery:
    "(max-width: 1200px)":
      grid-template-columns: 100%
      grid-template-areas: |
        "centre"
        "gauche"
        "droite"
cards:
  - view_layout:
      grid-area: gauche
    type: vertical-stack
    cards:
      - type: weather-forecast
        entity: weather.saint_denis_les_bourg
        name: Saint-Denis-lès-Bourg
        show_forecast: true
        show_current: true
        forecast_type: daily
      - type: custom:stack-in-card
        title: ⚡ BILAN ÉNERGÉTIQUE TEMPO
        keep:
          background: true
        cards:
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                entity: sensor.rte_tempo_couleur_actuelle
                name: Aujourd'hui
                show_label: true
                show_icon: true
                icon: mdi:flash
                label: >-
                  [[[ return 'Jour ' +
                  (states['sensor.rte_tempo_couleur_actuelle']?.state ||
                  'Inconnu'); ]]]
                styles:
                  card:
                    - height: 80px
                    - border-radius: 15px
                    - background-color: |
                        [[[
                          var c = states['sensor.rte_tempo_couleur_actuelle']?.state?.toLowerCase() || '';
                          if (c === 'rouge') return '#c62828 !important';
                          if (c === 'blanc') return '#f5f5f5 !important';
                          return '#1565c0 !important';
                        ]]]
                    - color: >-
                        [[[ return
                        (states['sensor.rte_tempo_couleur_actuelle']?.state?.toLowerCase()
                        === 'blanc') ? 'black' : 'white'; ]]]
                  grid:
                    - grid-template-areas: "\"i n\" \"i l\""
                    - grid-template-columns: 35% 1fr
                  icon:
                    - width: 35px
                  name:
                    - font-weight: bold
                    - font-size: 1.1em
                    - justify-self: start
                  label:
                    - font-size: 1em
                    - opacity: 0.9
                    - justify-self: start
              - type: custom:button-card
                entity: sensor.rte_tempo_prochaine_couleur
                name: Demain
                show_label: true
                show_icon: true
                icon: mdi:calendar-arrow-right
                label: >-
                  [[[ return 'Jour ' +
                  (states['sensor.rte_tempo_prochaine_couleur']?.state ||
                  'Inconnu'); ]]]
                styles:
                  card:
                    - height: 80px
                    - border-radius: 15px
                    - background-color: |
                        [[[
                          var c = states['sensor.rte_tempo_prochaine_couleur']?.state?.toLowerCase() || '';
                          if (c === 'rouge') return '#c62828 !important';
                          if (c === 'blanc') return '#f5f5f5 !important';
                          return '#1565c0 !important';
                        ]]]
                    - color: >-
                        [[[ return
                        (states['sensor.rte_tempo_prochaine_couleur']?.state?.toLowerCase()
                        === 'blanc') ? 'black' : 'white'; ]]]
                  grid:
                    - grid-template-areas: "\"i n\" \"i l\""
                    - grid-template-columns: 35% 1fr
                  icon:
                    - width: 35px
                  name:
                    - font-weight: bold
                    - font-size: 1.1em
                    - justify-self: start
                  label:
                    - font-size: 1em
                    - opacity: 0.9
                    - justify-self: start
          - type: custom:button-card
            entity: sensor.tarif_tempo_periode_actuelle
            show_name: false
            show_label: true
            label: |
              [[[
                var p = states['sensor.tarif_tempo_periode_actuelle'];
                var status = (p && p.state !== 'unknown') ? p.state : "Tempo";
                return "En " + status + " (HP 6h20-22h20 / HC 22h20-6h20)";
              ]]]
            styles:
              card:
                - height: 50px
                - margin: 5px
                - border-radius: 15px
                - background-color: rgba(255, 255, 255, 0.05) !important
              grid:
                - grid-template-areas: "\"i l\""
                - grid-template-columns: auto auto
                - justify-content: center
                - gap: 10px
              icon:
                - color: >-
                    [[[ return
                    (states['sensor.tarif_tempo_periode_actuelle']?.state?.includes('Heures
                    Pleines')) ? '#ffa600' : '#00d4ff'; ]]]
                - width: 22px
              label:
                - font-size: 0.95em
                - font-weight: bold
      - type: custom:stack-in-card
        title: COÛTS DU JOUR
        keep:
          background: true
        cards:
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                name: BLEU
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_journalier_bleu_hp']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_journalier_bleu_hc']?.state) || 0;
                    return "HP " + (hp * 0.1612).toFixed(2) + "€<br>HC " + (hc * 0.1325).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(21, 101, 192, 0.1) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(21, 101, 192, 0.3)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#1565c0"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
              - type: custom:button-card
                name: BLANC
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_journalier_blanc_hp']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_journalier_blanc_hc']?.state) || 0;
                    return "HP " + (hp * 0.1871).toFixed(2) + "€<br>HC " + (hc * 0.1499).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(245, 245, 245, 0.08) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(245, 245, 245, 0.2)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#f5f5f5"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
              - type: custom:button-card
                name: ROUGE
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_journalier_rouge_hp']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_journalier_rouge_hc']?.state) || 0;
                    return "HP " + (hp * 0.7060).toFixed(2) + "€<br>HC " + (hc * 0.1575).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(198, 40, 40, 0.1) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(198, 40, 40, 0.3)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#c62828"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
          - type: custom:button-card
            name: TOTAL JOUR
            triggers_update: all
            icon: mdi:lightning-bolt
            show_label: true
            label: |
              [[[
                var total = (parseFloat(states['sensor.suivi_tempo_journalier_bleu_hp']?.state) * 0.1612 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_journalier_bleu_hc']?.state) * 0.1325 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_journalier_blanc_hp']?.state) * 0.1871 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_journalier_blanc_hc']?.state) * 0.1499 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_journalier_rouge_hp']?.state) * 0.7060 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_journalier_rouge_hc']?.state) * 0.1575 || 0);
                return total.toFixed(2) + " €";
              ]]]
            styles:
              card:
                - height: 45px
                - background-color: rgba(255, 193, 7, 0.1) !important
                - border-radius: 10px
                - margin: 5px
              grid:
                - grid-template-areas: "\"i n l\""
                - grid-template-columns: 30px 1fr auto
              icon:
                - color: "#FFC107"
              name:
                - justify-self: start
                - font-size: 0.8em
                - font-weight: bold
                - opacity: 0.8
                - white-space: nowrap
              label:
                - justify-self: end
                - font-size: 1.1em
                - font-weight: bold
                - color: "#FFC107"
                - padding-right: 15px
      - type: custom:stack-in-card
        title: COÛTS DU MOIS
        keep:
          background: true
        cards:
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                name: BLEU
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_mensuel_bleu_hp']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_mensuel_bleu_hc']?.state) || 0;
                    return "HP " + (hp * 0.1612).toFixed(2) + "€<br>HC " + (hc * 0.1325).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(21, 101, 192, 0.1) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(21, 101, 192, 0.3)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#1565c0"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
              - type: custom:button-card
                name: BLANC
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_mensuel_blanc_hp']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_mensuel_blanc_hc']?.state) || 0;
                    return "HP " + (hp * 0.1871).toFixed(2) + "€<br>HC " + (hc * 0.1499).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(245, 245, 245, 0.08) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(245, 245, 245, 0.2)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#f5f5f5"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
              - type: custom:button-card
                name: ROUGE
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_mensuel_rouge_hp']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_mensuel_rouge_hc']?.state) || 0;
                    return "HP " + (hp * 0.7060).toFixed(2) + "€<br>HC " + (hc * 0.1575).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(198, 40, 40, 0.1) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(198, 40, 40, 0.3)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#c62828"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
          - type: custom:button-card
            name: TOTAL MOIS
            triggers_update: all
            icon: mdi:calendar-month
            show_label: true
            label: |
              [[[
                var total = (parseFloat(states['sensor.suivi_tempo_mensuel_bleu_hp']?.state) * 0.1612 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_mensuel_bleu_hc']?.state) * 0.1325 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_mensuel_blanc_hp']?.state) * 0.1871 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_mensuel_blanc_hc']?.state) * 0.1499 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_mensuel_rouge_hp']?.state) * 0.7060 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_mensuel_rouge_hc']?.state) * 0.1575 || 0);
                return total.toFixed(2) + " €";
              ]]]
            styles:
              card:
                - height: 45px
                - background-color: rgba(33, 150, 243, 0.15) !important
                - border-radius: 10px
                - margin: 5px
              grid:
                - grid-template-areas: "\"i n l\""
                - grid-template-columns: 30px 1fr auto
              icon:
                - color: "#2196F3"
              name:
                - justify-self: start
                - font-size: 0.8em
                - font-weight: bold
                - opacity: 0.8
                - white-space: nowrap
              label:
                - justify-self: end
                - font-size: 1.1em
                - font-weight: bold
                - color: "#2196F3"
                - padding-right: 15px
      - type: custom:stack-in-card
        title: COÛTS DE L'ANNÉE
        keep:
          background: true
        cards:
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                name: BLEU
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_annuel_hp_bleu']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_annuel_hc_bleu']?.state) || 0;
                    return "HP " + (hp * 0.1612).toFixed(2) + "€<br>HC " + (hc * 0.1325).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(21, 101, 192, 0.1) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(21, 101, 192, 0.3)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#1565c0"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
              - type: custom:button-card
                name: BLANC
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_annuel_hp_blanc']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_annuel_hc_blanc']?.state) || 0;
                    return "HP " + (hp * 0.1871).toFixed(2) + "€<br>HC " + (hc * 0.1499).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(245, 245, 245, 0.08) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(245, 245, 245, 0.2)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#f5f5f5"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
              - type: custom:button-card
                name: ROUGE
                triggers_update: all
                show_label: true
                label: |
                  [[[ 
                    var hp = parseFloat(states['sensor.suivi_tempo_annuel_hp_rouge']?.state) || 0;
                    var hc = parseFloat(states['sensor.suivi_tempo_annuel_hc_rouge']?.state) || 0;
                    return "HP " + (hp * 0.7060).toFixed(2) + "€<br>HC " + (hc * 0.1575).toFixed(2) + "€";
                  ]]]
                styles:
                  card:
                    - height: 70px
                    - background-color: rgba(198, 40, 40, 0.1) !important
                    - border-radius: 10px
                    - border: 1px solid rgba(198, 40, 40, 0.3)
                  grid:
                    - grid-template-areas: "\"n\" \"l\""
                    - grid-template-rows: 1fr 1fr
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
                    - color: "#c62828"
                  label:
                    - font-size: 0.85em
                    - line-height: 1.2
                    - text-align: center
          - type: custom:button-card
            name: TOTAL ANNÉE
            triggers_update: all
            icon: mdi:piggy-bank
            show_label: true
            label: |
              [[[
                var total = (parseFloat(states['sensor.suivi_tempo_annuel_hp_bleu']?.state) * 0.1612 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_annuel_hc_bleu']?.state) * 0.1325 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_annuel_hp_blanc']?.state) * 0.1871 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_annuel_hc_blanc']?.state) * 0.1499 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_annuel_hp_rouge']?.state) * 0.7060 || 0) +
                            (parseFloat(states['sensor.suivi_tempo_annuel_hc_rouge']?.state) * 0.1575 || 0);
                return total.toFixed(2) + " €";
              ]]]
            styles:
              card:
                - height: 45px
                - background-color: rgba(76, 175, 80, 0.1) !important
                - border-radius: 10px
                - margin: 5px
              grid:
                - grid-template-areas: "\"i n l\""
                - grid-template-columns: 30px 1fr auto
              icon:
                - color: "#4CAF50"
              name:
                - justify-self: start
                - font-size: 0.8em
                - font-weight: bold
                - opacity: 0.8
                - white-space: nowrap
              label:
                - justify-self: end
                - font-size: 1.1em
                - font-weight: bold
                - color: "#4CAF50"
                - padding-right: 15px
  - view_layout:
      grid-area: centre
    type: vertical-stack
    cards:
      - type: custom:power-flow-card-plus
        title: Flux Énergétique Réel
        entities:
          grid:
            name: Réseau EDF
            display_state: two_way
            entity:
              consumption: sensor.smart_meter_achat_sur_le_reseau
              production: sensor.smart_meter_alimentation_du_reseau
          solar:
            entity: sensor.production_solaire_totale
            name: Solaire
            display_zero_state: true
          battery:
            entity:
              consumption: sensor.batterie_decharge_totale
              production: sensor.batterie_charge_totale
            state_of_charge: sensor.batterie_soc_moyen
            name: Solarbanks
            color_icon: true
          home:
            entity: sensor.smart_meter_achat_sur_le_reseau
            name: Maison
            circle_animation: true
            subtract_individual: true
          individual:
            - entity: sensor.chauffe_eau_puissance
              name: Chauffe-eau
              icon: mdi:water-boiler
              display_zero_state: true
              display_zero: true
              unit_of_measurement: W
              decimals: 2
        w_decimals: 0
        kw_decimals: 1
        display_zero_lines: true
        clickable_entities: true
      - type: custom:button-card
        name: Énergie de la Maison
        triggers_update: all
        styles:
          card:
            - margin-top: 10px
            - height: 260px
            - border-radius: 25px
            - background-color: "#1c1c1e !important"
            - padding: 20px
            - color: white
          grid:
            - grid-template-areas: "\"n\" \"s\" \"b\" \"h\""
            - grid-template-rows: min-content 1fr 1fr 1fr
          name:
            - font-weight: bold
            - font-size: 1.15em
            - margin-bottom: 10px
        extra_styles: >
          .resp-icon { width: 30px; height: 30px; margin-right: 12px; }

          .resp-text { font-size: 1.05em; font-weight: 600; }

          .resp-val { font-size: 1.35em; font-weight: bold; }

          .resp-val-soc { font-size: 1.2em; font-weight: bold; }

          .resp-margin { margin-top: 10px; }

          .resp-border { margin-top: 15px; padding-top: 15px; border-top: 2px
          solid #3a3a3c; }

          .resp-bar { height: 10px; background: #3a3a3c; border-radius: 5px;
          overflow: hidden; }


          /* Mode Téléphone UNIQUEMENT */

          @media (max-width: 1200px) {
            #card { height: auto !important; padding: 15px !important; margin-top: 0px !important; }
            #name { font-size: 1em !important; margin-bottom: 5px !important; }
            .resp-icon { width: 22px; height: 22px; margin-right: 8px; }
            .resp-text { font-size: 0.85em; }
            .resp-val { font-size: 1.1em; }
            .resp-val-soc { font-size: 1em; }
            .resp-margin { margin-top: 8px !important; }
            .resp-border { margin-top: 12px !important; padding-top: 12px !important; }
            .resp-bar { height: 8px !important; }
          }
        custom_fields:
          s: |
            [[[
              var pwr = states['sensor.production_solaire_totale']?.state || 0;
              return `
                <div style="width: 100%; display: flex; justify-content: space-between; align-items: center;">
                  <div style="display: flex; align-items: center;">
                    <ha-icon icon="mdi:solar-power" style="color: #FFD60A;" class="resp-icon"></ha-icon>
                    <span class="resp-text">Production Totale</span>
                  </div>
                  <span style="color: #FFD60A;" class="resp-val">${pwr} W</span>
                </div>
              `
            ]]]
          b: |
            [[[
              var soc = states['sensor.batterie_soc_moyen']?.state || 0;
              var charge = parseFloat(states['sensor.batterie_charge_totale']?.state) || 0;
              var decharge = parseFloat(states['sensor.batterie_decharge_totale']?.state) || 0;
              var pwr_batt = charge > 0 ? charge : -decharge;
              return `
                <div style="width: 100%;" class="resp-margin">
                  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
                    <div style="display: flex; align-items: center;">
                      <ha-icon icon="mdi:battery-high" style="color: #0A84FF;" class="resp-icon"></ha-icon>
                      <span class="resp-text">Stockage Global (${pwr_batt} W)</span>
                    </div>
                    <span style="color: #0A84FF;" class="resp-val-soc">${soc}%</span>
                  </div>
                  <div style="width: 100%;" class="resp-bar">
                    <div style="width: ${soc}%; height: 100%; background: #0A84FF; border-radius: 5px; transition: width 1s;"></div>
                  </div>
                </div>
              `
            ]]]
          h: |
            [[[
              var grid = states['sensor.smart_meter_achat_sur_le_reseau']?.state || 0;
              return `
                <div style="width: 100%; display: flex; justify-content: space-between; align-items: center;" class="resp-border">
                  <div style="display: flex; align-items: center;">
                    <ha-icon icon="mdi:transmission-tower" style="color: #f5f5f5;" class="resp-icon"></ha-icon>
                    <span class="resp-text">Réseau EDF</span>
                  </div>
                  <span style="color: #f5f5f5;" class="resp-val">${grid} W</span>
                </div>
              `
            ]]]
      - type: custom:apexcharts-card
        header:
          show: true
          title: Suivi Production/Consommation
          show_states: true
          colorize_states: true
        graph_span: 24h
        apex_config:
          chart:
            height: 400
          stroke:
            curve: smooth
            dashArray:
              - 0
              - 0
              - 0
              - 5
          legend:
            show: false
          dataLabels:
            enabled: false
        series:
          - entity: sensor.production_solaire_totale
            name: Solaire
            type: area
            color: "#e68a00"
            opacity: 0.3
            stroke_width: 1
            group_by:
              func: avg
              duration: 15m
          - entity: sensor.smart_meter_achat_sur_le_reseau
            name: Réseau (Achat)
            type: area
            color: "#3477a3"
            opacity: 0.3
            stroke_width: 1
            group_by:
              func: avg
              duration: 15m
          - entity: sensor.smart_meter_alimentation_du_reseau
            name: Réseau (Injection)
            type: area
            color: "#3477a3"
            opacity: 0.3
            transform: return x * -1;
            stroke_width: 1
            group_by:
              func: avg
              duration: 15m
          - entity: sensor.consommation_totale_et
            name: Consommation (hors solaire)
            type: line
            color: "#ffffff"
            stroke_width: 1
            group_by:
              func: avg
              duration: 15m
  - view_layout:
      grid-area: droite
    type: vertical-stack
    cards:
      - type: custom:stack-in-card
        title: 🚥 AUTORISATION ÉLECTROMÉNAGER
        keep:
          background: true
        cards:
          - type: horizontal-stack
            cards:
              - type: custom:button-card
                name: Lave-Linge
                icon: mdi:washing-machine
                show_state: false
                triggers_update: all
                styles:
                  card:
                    - height: 70px
                    - border-radius: 10px
                    - margin: 5px
                    - background-color: |
                        [[[
                          var soc = parseFloat(states['sensor.batterie_soc_moyen']?.state) || 0;
                          var prod = parseFloat(states['sensor.production_solaire_totale']?.state) || 0;
                          return (soc > 90 && prod >= 2000) ? 'rgba(76, 175, 80, 0.2) !important' : 'rgba(158, 158, 158, 0.1) !important';
                        ]]]
                  icon:
                    - color: |
                        [[[
                          var soc = parseFloat(states['sensor.batterie_soc_moyen']?.state) || 0;
                          var prod = parseFloat(states['sensor.production_solaire_totale']?.state) || 0;
                          return (soc > 90 && prod >= 2000) ? '#4CAF50' : 'grey';
                        ]]]
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
              - type: custom:button-card
                name: Lave-Vaiss.
                icon: mdi:dishwasher
                show_state: false
                triggers_update: all
                styles:
                  card:
                    - height: 70px
                    - border-radius: 10px
                    - margin: 5px
                    - background-color: |
                        [[[
                          var soc = parseFloat(states['sensor.batterie_soc_moyen']?.state) || 0;
                          var prod = parseFloat(states['sensor.production_solaire_totale']?.state) || 0;
                          return (soc > 90 && prod >= 2000) ? 'rgba(76, 175, 80, 0.2) !important' : 'rgba(158, 158, 158, 0.1) !important';
                        ]]]
                  icon:
                    - color: |
                        [[[
                          var soc = parseFloat(states['sensor.batterie_soc_moyen']?.state) || 0;
                          var prod = parseFloat(states['sensor.production_solaire_totale']?.state) || 0;
                          return (soc > 90 && prod >= 2000) ? '#4CAF50' : 'grey';
                        ]]]
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
              - type: custom:button-card
                name: Sèche-Linge
                icon: mdi:tumble-dryer
                show_state: false
                triggers_update: all
                styles:
                  card:
                    - height: 70px
                    - border-radius: 10px
                    - margin: 5px
                    - background-color: |
                        [[[
                          var soc = parseFloat(states['sensor.batterie_soc_moyen']?.state) || 0;
                          var prod = parseFloat(states['sensor.production_solaire_totale']?.state) || 0;
                          return (soc > 90 && prod >= 2000) ? 'rgba(76, 175, 80, 0.2) !important' : 'rgba(158, 158, 158, 0.1) !important';
                        ]]]
                  icon:
                    - color: |
                        [[[
                          var soc = parseFloat(states['sensor.batterie_soc_moyen']?.state) || 0;
                          var prod = parseFloat(states['sensor.production_solaire_totale']?.state) || 0;
                          return (soc > 90 && prod >= 2000) ? '#4CAF50' : 'grey';
                        ]]]
                  name:
                    - font-size: 0.8em
                    - font-weight: bold
      - type: custom:stack-in-card
        title: 🌡️ CONFORT THERMIQUE
        keep:
          background: true
        cards:
          - type: grid
            columns: 2
            square: false
            cards:
              - type: custom:button-card
                name: Salon
                icon: mdi:thermometer
                show_label: true
                label: |
                  [[[
                    var t = states['sensor.salon_temperature']?.state;
                    var h = states['sensor.salon_humidite']?.state;
                    var temp = t && !isNaN(t) ? parseFloat(t).toFixed(1) : '--';
                    var hum = h && !isNaN(h) ? parseFloat(h).toFixed(0) : '--';
                    return temp + ' °C &nbsp;|&nbsp; 💧 ' + hum + ' %';
                  ]]]
                styles:
                  card:
                    - height: 60px
                    - border-radius: 12px
                    - background-color: >
                        [[[ return
                        (parseFloat(states['sensor.radiateur_salon_demande_instantanee']?.state)
                        > 10) ? 'rgba(255, 152, 0, 0.15) !important' :
                        'rgba(255, 255, 255, 0.05) !important'; ]]]
                  grid:
                    - grid-template-areas: "\"i n\" \"i l\""
                    - grid-template-columns: 40px 1fr
                  icon:
                    - color: >
                        [[[ return
                        (parseFloat(states['sensor.radiateur_salon_demande_instantanee']?.state)
                        > 10) ? '#FF9800' : '#2196F3'; ]]]
                    - width: 24px
                  name:
                    - justify-self: start
                    - font-weight: bold
                    - font-size: 0.9em
                  label:
                    - justify-self: start
                    - font-size: 0.85em
                    - opacity: 0.8
              - type: custom:button-card
                name: Parent
                icon: mdi:thermometer
                show_label: true
                label: |
                  [[[
                    var t = states['sensor.thermometre_chambre_parent_temperature']?.state;
                    var h = states['sensor.thermometre_chambre_parent_humidite']?.state;
                    var temp = t && !isNaN(t) ? parseFloat(t).toFixed(1) : '--';
                    var hum = h && !isNaN(h) ? parseFloat(h).toFixed(0) : '--';
                    return temp + ' °C &nbsp;|&nbsp; 💧 ' + hum + ' %';
                  ]]]
                styles:
                  card:
                    - height: 60px
                    - border-radius: 12px
                    - background-color: >
                        [[[ return
                        (parseFloat(states['sensor.radiateur_parent_demande_instantanee']?.state)
                        > 10) ? 'rgba(255, 152, 0, 0.15) !important' :
                        'rgba(255, 255, 255, 0.05) !important'; ]]]
                  grid:
                    - grid-template-areas: "\"i n\" \"i l\""
                    - grid-template-columns: 40px 1fr
                  icon:
                    - color: >
                        [[[ return
                        (parseFloat(states['sensor.radiateur_parent_demande_instantanee']?.state)
                        > 10) ? '#FF9800' : '#2196F3'; ]]]
                    - width: 24px
                  name:
                    - justify-self: start
                    - font-weight: bold
                    - font-size: 0.9em
                  label:
                    - justify-self: start
                    - font-size: 0.85em
                    - opacity: 0.8
              - type: custom:button-card
                name: Enfants
                icon: mdi:thermometer
                show_label: true
                label: |
                  [[[
                    var t = states['sensor.thermometre_enfants_temperature']?.state;
                    var h = states['sensor.thermometre_enfants_humidite']?.state;
                    var temp = t && !isNaN(t) ? parseFloat(t).toFixed(1) : '--';
                    var hum = h && !isNaN(h) ? parseFloat(h).toFixed(0) : '--';
                    return temp + ' °C &nbsp;|&nbsp; 💧 ' + hum + ' %';
                  ]]]
                styles:
                  card:
                    - height: 60px
                    - border-radius: 12px
                    - background-color: >
                        [[[ return
                        (parseFloat(states['sensor.radiateur_enfant_demande_instantanee']?.state)
                        > 10) ? 'rgba(255, 152, 0, 0.15) !important' :
                        'rgba(255, 255, 255, 0.05) !important'; ]]]
                  grid:
                    - grid-template-areas: "\"i n\" \"i l\""
                    - grid-template-columns: 40px 1fr
                  icon:
                    - color: >
                        [[[ return
                        (parseFloat(states['sensor.radiateur_enfant_demande_instantanee']?.state)
                        > 10) ? '#FF9800' : '#2196F3'; ]]]
                    - width: 24px
                  name:
                    - justify-self: start
                    - font-weight: bold
                    - font-size: 0.9em
                  label:
                    - justify-self: start
                    - font-size: 0.85em
                    - opacity: 0.8
              - type: custom:button-card
                name: Studio
                icon: mdi:thermometer
                show_label: true
                label: |
                  [[[
                    var t = states['sensor.studio_temperature']?.state;
                    var h = states['sensor.studio_humidite']?.state;
                    var temp = t && !isNaN(t) ? parseFloat(t).toFixed(1) : '--';
                    var hum = h && !isNaN(h) ? parseFloat(h).toFixed(0) : '--';
                    return temp + ' °C &nbsp;|&nbsp; 💧 ' + hum + ' %';
                  ]]]
                styles:
                  card:
                    - height: 60px
                    - border-radius: 12px
                    - background-color: |
                        [[[ 
                          var s1 = parseFloat(states['sensor.smart_wi_fi_plug_puissance']?.state) || 0;
                          var s2 = parseFloat(states['sensor.radiateur_bureau_demande_instantanee']?.state) || 0;
                          var s3 = parseFloat(states['sensor.radiateur_studio_demande_instantanee']?.state) || 0;
                          if (s1 > 10 || s2 > 10 || s3 > 10) return 'rgba(255, 152, 0, 0.15) !important';
                          return 'rgba(255, 255, 255, 0.05) !important';
                        ]]]
                  grid:
                    - grid-template-areas: "\"i n\" \"i l\""
                    - grid-template-columns: 40px 1fr
                  icon:
                    - color: |
                        [[[ 
                          var s1 = parseFloat(states['sensor.smart_wi_fi_plug_puissance']?.state) || 0;
                          var s2 = parseFloat(states['sensor.radiateur_bureau_demande_instantanee']?.state) || 0;
                          var s3 = parseFloat(states['sensor.radiateur_studio_demande_instantanee']?.state) || 0;
                          if (s1 > 10 || s2 > 10 || s3 > 10) return '#FF9800';
                          return '#2196F3';
                        ]]]
                    - width: 24px
                  name:
                    - justify-self: start
                    - font-weight: bold
                    - font-size: 0.9em
                  label:
                    - justify-self: start
                    - font-size: 0.85em
                    - opacity: 0.8
      - type: custom:stack-in-card
        title: 🛡️ SÉCURITÉ & ACCÈS
        cards:
          - type: entities
            entities:
              - entity: alarm_control_panel.maison_alarm
                name: Centrale Alarme Ring
              - entity: binary_sensor.porte_d_entree
                name: Porte d'Entrée
                icon: mdi:door
              - entity: binary_sensor.porte_de_derriere
                name: Porte de Derrière
                icon: mdi:door
              - entity: binary_sensor.porte_de_garage
                name: Porte de Garage
                icon: mdi:garage
              - entity: binary_sensor.fenetre_avant
                name: Fenêtre Avant
                icon: mdi:window-closed
              - entity: binary_sensor.fenetre_laterale
                name: Fenêtre Latérale
                icon: mdi:window-closed
              - entity: binary_sensor.detecteur_de_mouvement_d_entree
                name: Mouvement Entrée
                icon: mdi:motion-sensor
              - entity: binary_sensor.detecteur_de_mouvement_garage
                name: Mouvement Garage
                icon: mdi:motion-sensor
              - entity: binary_sensor.detecteur_de_mouvement_studio
                name: Mouvement Studio
                icon: mdi:motion-sensor
      - type: custom:stack-in-card
        title: 🤖 DÉLESTAGE AUTOMATIQUE
        cards:
          - type: entities
            entities:
              - entity: automation.tempo_bascule_des_tarifs
                name: Gestion Tarifs (Auto)
              - entity: automation.chauffe_eau_on_solaire
                name: Auto - Allumage ECS
              - entity: automation.chauffe_eau_off_solaire
                name: Auto - Coupure ECS
              - entity: switch.chauffe_eau_mise_a_jour_automatique
                name: Commutateur ECS
      - type: custom:scheduler-card
        title: 📅 PLANIFICATEUR CHAUFFAGE
        include:
          - climate
          - switch
        display_options:
          primary_info: name
          secondary_info: relative-time

Merci pour la lecture ! J’espère que ce partage inspirera ceux qui veulent passer d’une domotique de confort à une véritable gestion énergétique industrielle.

Des bisous ! :chequered_flag::fire:

Bonjour,
La règle est juste une carte à présenter et pas une page complète d’un dashboard.

Salut WarC0zes,

​Techniquement, c’est bien une carte unique. Sous Home Assistant, une carte ne se limite pas à un simple widget monobloc ou une tuile isolée ; c’est avant tout une unité de configuration Lovelace.

​Tout mon système est encapsulé sous une racine unique : type: custom:layout-card. Ce conteneur maître définit l’intelligence, la structure et le responsive. Séparer ces éléments en cartes distinctes n’aurait aucun sens technique, car elles forment un tout indissociable : mon algorithme « Feu Vert » à droite est asservi en temps réel aux données de flux du centre.

​C’est donc une proposition de Master Card unique de supervision : un seul bloc YAML complet pour une fonction de pilotage cohérente. :hammer_and_wrench::robot:

Au final je suis dans les règles…

2 « J'aime »

Même si peut-être hors concours, merci pour ce partage qui est inspirant. Ca va en aider plus d’un !

2 « J'aime »