[Tutoriel] Prévision des couleurs Tempo à J+9

quand tu regardes les statistiques globales ce n’est pas si mauvais que ça:

1 « J'aime »

Bonjour

Meilleurs Vœux pour 2026 à tous et Grand MERCI !!

Résultat super basique mais fonctionnel pour moi :grin:

image

1 « J'aime »

Salut,

Je me doutais que cette demande arriverait :slight_smile:
D’où ma proposition d’un paramètre optionnel au widget ci-dessous :

nbJours : Définit le nombre de jours affichés sur le widget (ex: 3). Valeur par défaut : 9.

Code du widget Tempo avec paramètre optionnel nbJours
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="other" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#">
  <!-- ################ nooTempo ################ 
       ********** Paramètres Optionnels ***************
		
        nbJours : Définit le nombre de jours affichés sur le widget (ex: 3). Valeur par défaut : 9.

        Pensez au café pour les nuits blanches de codage ;) 
        https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=noodom.fr%40gmail.com&currency_code=EUR&source=url
       ########### by @noodom ;)  ############# -->

  <div class="title #hide_name#">
    <div class="cmdName" style="font-weight: bold; margin-bottom: 5px;">#name_display#</div>
  </div>
  <div class="tempo-forecast-wrapper" id="tempo_container_#id#"></div>

  <template>
     <div>
        <div>nbJours : Définit le nombre de jours affichés sur le widget (ex: 3). Valeur par défaut : 9.</div>
     </div>
  </template>

  <style>
    .tempo-forecast-wrapper {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      justify-content: center;
      padding: 5px;
    }
    .tempo-day-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      border-radius: 4px;
      padding: 4px;
      min-width: 55px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.3);
    }
    .t-date { font-size: 0.75em; font-weight: bold; margin-bottom: 2px; }
    .t-prob { font-size: 0.7em; font-style: italic; margin-top: 2px; opacity: 0.8; }
    
    /* Styles des couleurs Tempo */
    .t-bleu { background-color: #0055b8 !important; color: white !important; }
    .t-blanc { background-color: #f0f0f0 !important; color: #333 !important; border: 1px solid #ccc; }
    .t-rouge { background-color: #ce0501 !important; color: white !important; }
  </style>

  <script>
    jeedom.cmd.update['#id#'] = function(_options) {
      try {
        let data = JSON.parse(_options.display_value);
        let container = $('#tempo_container_#id#');
        container.empty();

        // Gestion du paramètre optionnel nbJours
        let limit = ('#nbJours#' != '#' + 'nbJours#') ? parseInt('#nbJours#') : 9;
        let displayData = data.slice(0, limit);

        displayData.forEach(item => {
          // Formatage date YYYY-MM-DD vers DD/MM
          let dateParts = item.date.split('-');
          let shortDate = dateParts[2] + '/' + dateParts[1];
          
          // Formatage probabilité (0.99 -> 99%)
          let proba = Math.round(item.probability * 100) + '%';
          
          container.append(`
            <div class="tempo-day-card t-${item.couleur.toLowerCase()}">
              <span class="t-date">${shortDate}</span>
              <span class="t-prob">${proba}</span>
            </div>
          `);
        });
      } catch (e) {
        $('#tempo_container_#id#').html('<span style="font-size:0.8em;">Attente de données valides...</span>');
      }
    }
    jeedom.cmd.update['#id#']({display_value:'#state#'});
  </script>
</div>
1 « J'aime »

Hello,
super @Bison :+1:
Entre ton tuto script + widget et un virtuel je suis parvenu à faire sur un design un truc discret et efficace
image
Merci

3 « J'aime »

Une variante en prenant un json plus complet : https://open-dpe.fr/assets/tempo_days.json

  • affichage de la consommation brute nationale prévue par RTE, consommation réelle à fournir par les centrales, apport estimé du solaire et de l’éolien, probabilité des couleurs par jour, jours blancs et rouges restants pour la saison
  • description des champs affichés par infobulle
  • possibilité d’afficher ou non les différents champs par paramètres optionnels
  • affichage proportionnel ou non des pourcentages de couleur

Ajout de paramètres optionnels :

Avec des champs cachés (par paramètres optionnels) :

Il suffit de reprendre le post [Tutoriel] Prévision des couleurs Tempo à J+9 - #7 par noodom avec le contenu suivant :

Code du fichier tempo.php
<?php
//$url = "https://open-dpe.fr/assets/tempo_days_lite.json";
$url = "https://open-dpe.fr/assets/tempo_days.json";
$json = file_get_contents($url);

if ($json === false) {
    echo "Erreur de récupération";
} else {
    echo $json;
}
?>
Code du widget
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="other" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#">
  <!-- ################ nooTempo ################ 
       ********** Paramètres Optionnels ***************
		
         nbJours : Nombre de jours affichés (Défaut 9).
         showTotal : Afficher la ligne Total Brut (1:oui, 0:non).
         showBalance : Afficher la barre Net/EnR et sa légende (1:oui, 0:non).
         showStocks : Afficher Jours restants (1:oui, 0:non).
         proportional : Affichage probabilités proportionnel (1:oui, 0:non).

        Pensez au café pour les nuits blanches de codage ;) 
        https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=noodom.fr%40gmail.com&currency_code=EUR&source=url
       ########### by @noodom ;)  ############# -->
  
  <div class="title #hide_name#">
    <div class="cmdName" style="font-weight: bold; margin-bottom: 5px;">#name_display#</div>
  </div>
  
  <div class="tempo-forecast-wrapper" id="tempo_container_#id#"></div>

  <template>
      <div>
         <div>nbJours : Nombre de jours affichés (Défaut 9).</div>
         <div>showTotal : Afficher la ligne Total Brut (1:oui, 0:non).</div>
         <div>showBalance : Afficher la barre Net/EnR et sa légende (1:oui, 0:non).</div>
         <div>showStocks : Afficher Jours restants (1:oui, 0:non).</div>
         <div>proportional : Affichage probabilités proportionnel (1:oui, 0:non).</div>
      </div>
  </template>

  <style>
    .tempo-forecast-wrapper {
      display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; padding: 10px;
    }
    .tempo-day-card {
      display: flex; flex-direction: column; align-items: center; border-radius: 10px;
      padding: 10px; min-width: 135px; box-shadow: 0 4px 6px rgba(0,0,0,0.3);
      border: 1px solid rgba(255,255,255,0.15);
    }
    .t-date { font-size: 1em; font-weight: bold; margin-bottom: 8px; border-bottom: 1px solid rgba(0,0,0,0.1); width: 100%; text-align: center; }
    
    .t-power-zone { font-size: 0.75em; margin-bottom: 8px; width: 100%; }
    .t-power-line { display: flex; justify-content: space-between; margin-bottom: 4px; cursor: help; }
    
    .t-balance-container { margin: 8px 0; width: 100%; }
    .t-balance-bar {
      display: flex; width: 100%; height: 8px; background: rgba(0,0,0,0.1);
      border-radius: 4px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05); cursor: help;
    }
    .bar-net { background: #2d3436; height: 100%; }
    .bar-enr { background: #00b894; height: 100%; }
    
    .t-balance-legend {
      display: flex; justify-content: space-between; font-size: 0.85em; 
      margin-top: 4px; line-height: 1.1;
    }
    .leg-item { display: flex; flex-direction: column; cursor: help; }
    .leg-label { font-size: 0.75em; opacity: 0.8; margin-bottom: 1px; }
    .leg-net { font-weight: bold; text-align: left; }
    .leg-enr { font-weight: 500; text-align: right; }
    .t-arrow-down { font-size: 0.9em; font-weight: bold; margin-right: 1px; }

    .t-probas-row {
      display: flex; gap: 3px; margin-bottom: 10px; background: rgba(0,0,0,0.15);
      padding: 4px; border-radius: 6px; min-height: 28px; width:100%; box-sizing: border-box; align-items: center; justify-content: center;
    }
    .t-mini-box {
      display: inline-flex !important; align-items: center !important; justify-content: center !important;
      height: 20px !important; border-radius: 3px !important; font-size: 8px !important; font-weight: 900 !important; color: white !important;
    }
    .t-fixe { width: 20px !important; }
    .t-prop { width: 100%; }

    .t-bleu { background-color: #0055b8 !important; color: white !important; }
    .t-blanc { background-color: #f8f9fa !important; color: #1e272e !important; }
    .t-rouge { background-color: #ce0501 !important; color: white !important; }
    
    .mini-b { background-color: #003a7e !important; }
    .mini-w { background-color: #ffffff !important; color: #1e272e !important; border: 1px solid #ced4da; }
    .mini-r { background-color: #920401 !important; }

    .t-stock-footer {
      display: flex; gap: 8px; font-size: 0.75em; font-weight: bold; border-top: 1px solid rgba(0,0,0,0.1);
      padding-top: 6px; width: 100%; justify-content: space-around;
    }
  </style>

  <script>
    jeedom.cmd.update['#id#'] = function(_options) {
      try {
        let data = JSON.parse(_options.display_value);
        let container = $('#tempo_container_#id#');
        container.empty();

        let limit = (is_numeric('#nbJours#')) ? parseInt('#nbJours#') : 9;
        let showTotal = ('#showTotal#' == '0') ? false : true;
        let showBalance = ('#showBalance#' == '0') ? false : true;
        let showStocks = ('#showStocks#' == '0') ? false : true;
        let isProp = ('#proportional#' == '1') ? true : false;

        data.slice(0, limit).forEach(item => {
          let dateParts = item.date.split('-');
          let shortDate = dateParts[2] + '/' + dateParts[1];
          
          let f_gross = (item.forecast / 1000).toFixed(1);
          let f_net = (item.consumption_net / 1000).toFixed(1);
          let f_enr = (f_gross - f_net).toFixed(1);
          
          let pctNet = (item.consumption_net / item.forecast * 100).toFixed(0);
          let pctEnR = (100 - pctNet).toFixed(0);

          let html = `<div class="tempo-day-card t-${item.tempo_color.toLowerCase()}">`;
          html += `<span class="t-date">${shortDate}</span>`;
          
          if (showTotal || showBalance) {
            html += `<div class="t-power-zone">`;
            
            // PARAMETRE 1 : LIGNE TOTAL
            if (showTotal) {
              html += `<div class="t-power-line" title="Consommation brute totale prévue sur le réseau (en Gigawatts)"><span>Total:</span><b>${f_gross} GW</b></div>`;
            }
            
            // PARAMETRE 2 : BARRE + NET + ENR
            if (showBalance) {
              html += `<div class="t-balance-container">
                        <div class="t-balance-bar" title="Répartition : ${pctNet}% Pilotable / ${pctEnR}% Renouvelable">
                          <div class="bar-net" style="width:${pctNet}%"></div>
                          <div class="bar-enr" style="width:${pctEnR}%"></div>
                        </div>
                        <div class="t-balance-legend">
                          <div class="leg-item" title="Consommation NETTE : effort réel demandé aux centrales pilotables (Nucléaire, Gaz, etc.)">
                             <span class="leg-label">Net</span>
                             <span class="leg-net">${f_net}G</span>
                          </div>
                          <div class="leg-item" title="Apport EnR : part de la consommation couverte par le Solaire et l'Éolien (déduite du Total)">
                             <span class="leg-label">EnR</span>
                             <span class="leg-enr"><span class="t-arrow-down">↓</span>${f_enr}G</span>
                          </div>
                        </div>
                      </div>`;
            }
            html += `</div>`;
          }

          html += `<div class="t-probas-row ${isProp ? 't-prop' : ''}">`;
          const probas = [
            {v:Math.round(item.probability_bleu * 100),c:'mini-b',t:'Bleu'},
            {v:Math.round(item.probability_blanc * 100),c:'mini-w',t:'Blanc'},
            {v:Math.round(item.probability_rouge * 100),c:'mini-r',t:'Rouge'}
          ];
          probas.forEach(p => {
            if (isProp) {
              if (p.v > 0) html += `<div class="t-mini-box ${p.c}" style="flex:${p.v};margin:0 1px;" title="Probabilité ${p.t}: ${p.v}%">${p.v}</div>`;
            } else {
              html += `<div class="t-mini-box ${p.c} t-fixe" style="margin:0 2px;" title="Probabilité ${p.t}: ${p.v}%">${p.v}</div>`;
            }
          });
          html += `</div>`;

          if (showStocks) {
            html += `<div class="t-stock-footer">
                      <div title="Jours Blancs restants dans la saison (Total 43)">⚪ ${item.stock_blanc}</div>
                      <div title="Jours Rouges restants dans la saison (Total 22)">🔴 ${item.stock_rouge}</div>
                    </div>`;
          }

          html += `</div>`;
          container.append(html);
        });
      } catch (e) {
        $('#tempo_container_#id#').html('Erreur JSON');
      }
    }
    jeedom.cmd.update['#id#']({display_value:'#state#'});
  </script>
</div>
4 « J'aime »

Je l’avais trouvé hier soir ce json. J’allais proposé un truc dans la journée parce que c’est intéressant d’avoir au moins l’autre couleur qui est envisagé en plus de la couleur dominante.

Je vais peut-être m’abstenir, ça va faire beaucoup de possibilités sinon.

Merci d’avoir travaillé dessus

1 « J'aime »

Je l’avais trouvé aussi mais je ne voyais pas trop quoi en faire, super :+1:

Sinon, y aurait moyen d’avoir aussi un tableau dans ce genre avec juste les 9 derniers jours (ça nécessite l’utilisation de commandes historisées). Ça permet de visualiser l’évolution de la couleur d’un jour

Surtout avec la première prévision pour demain 12 Janvier où le bleu avait 0%, le blanc 80% et le rouge 12%

image

Au final, c’est le l’outsider le bleu qui gagne :rofl:

Il faut faire la différence entre les prévisions basées sur des données et la politique tarifaire qui va imposer des choix différents.
Il ne reste que 40 jours blancs ou rouges pour couvrir la fin de la période froide du 15 janvier au 15 avril et il ne s’agit pas actuellement de réduire les pics de consommation mais d’optimiser les gains

1 « J'aime »

Hello,

En effet, le résultat est quand même surprenant donc il doit y avoir une autre considération que celle liée à la « tension » sur le réseau. Et puis j’imagine qu’il y a aussi des éléments que seul RTE doit pouvoir évaluer comme le besoin de fournir de l’énergie à nos voisins européens et là … je pense que ça devient juste impossible de tomber bon.

On verra sur la longue ce que ça donne. Si le modèle est trop pourri et qu’il ne sert à rien on cliquera sur le bouton « Supprimer » :joy:

Là où on pourra douter du modèle c’est si on a une prévision en bleu mais finalement un jour rouge (quoique ça puisse arriver en fin de saison pour épuiser les jours rouges restants)
Peut-être il y a-t-il des prévisions à long terme pour un hivers rigoureux ?
Sinon c’est de la gestion de stock !

Disons qu’il faudrait savoir d’où viennent les données et qui est derrière ce site… Soit il a accès aux algos de rte, soit il fait ces prévisions comme ma grand-mère et ses escargots qui rampent sur le dos pour annoncer de grosses pluies :sweat_smile:

De toute façon, il y a des prévisions qui ne vont pas fondamentalement dans les données… On ne peut pas annoncer un jour rouge à 100% de proba (donc c’est sûr que ce sera ça !) à j+8 ou 9 et annoncer qu’il sera peut-être rouge à xx % ensuite voir bleu ou blanc …

Un tableau d’analyse des résultats en dit souvent plus long que de longs discours :sweat_smile:

https://open-dpe.fr/assets/tempo_results-dark.svg

Bon, en même temps, de mon côté, je mets juste à disposition un widget à la communauté que je n’utiliserai pas, n’ayant pas Tempo :upside_down_face:Mais si ça peut être utile…

2 « J'aime »

Bonjour à tous,
Pour ceux qui ne l’auraient pas déjà vu et consulté, l’algorithme de détermination de la couleur du jour est là : Méthode de choix des jours Tempo dans la page Calendrier des offres de fourniture de type Tempo.
Pour l’avoir exploré il y a quelques temps, il me semble que toutes les données de base de l’algorithme sont publiques, mais … … d’exploitation difficile. Et, bien sur, il y a comme paramètre de l’algorithme le nombre de jours restants par couleur.
Si çà peut servir …

Hello,

C’est très intéressant merci pour l’information.

On peut donc supposer que l’algorithme utilisé connait et utilise ce même calcul.

Par contre le problème principal doit être que « RTE ne publie pas les prévisions de production d’énergie éolienne et solaire. »

Les estimations de ces 2 valeurs sont donc réalisées sur la base des prévisions de météo France (comme indiqué sur le site) et c’est forcément l’explication du passage d’une prévision d’un rouge à un bleu à J+3 par exemple.

Quand on sait tous, d’expérience, comme il est difficile d’avoir des estimations fiables à plusieurs jours (voir même le jour J) de la météo, pas trop étonnant que les estimations des couleurs Tempo soient variables

1 « J'aime »

En fait, en creusant un peu, il existe bien des prévisions de production dont énergies renouvelables sur le site RTE (Prévisions de production).
Mais … … si je décode correctement l’information disponible, à aujourd’hui, nous avons à disposition les prévisions pour … … aujourd’hui :grin:.

1 « J'aime »

Je regarderai entre 18h et 20h, on dirait qu’il reçoivent des informations pour le lendemain à ce moment

2 « J'aime »

Genial merci c’est top

3 « J'aime »

plugin-rteecowatt utilise l’API consumption de RTE pour les consommations France:
image
On a les conso prévues à 2 jours ( en rouge et non fonctionnel sur le graphique ).
Il y aussi les prévisions sur 9 jours mais je ne les ai pas intégrées car basées sur la météo.

Bien qu’ayant les prévisions sur 2 jours (pas en ce moment), je ne me suis pas lancé dans les prévisions de couleur Tempo.
Les seules prévisions que je maitrise sont bleu le dimanche et bleu quand il n’y a plus de jour blanc/rouge. :wink: :innocent:

La non disponibilité des prévisions à 2 jours peut peut-etre expliquer pourquoi c’est encore prévu blanc le 12/01 chez OpenDPE alors que c’est bleu depuis 10h30.
image

L’algo de choix des couleurs par RTE

2 « J'aime »

Nouvelle version qui ajoute :

  • l’affichage du coût de l’abonnement en fonction de la puissance de l’abonnement
  • des heures pleines et creuses associées pour chaque couleur.

Il suffit d’ajouter le paramètre optionnel : puissance (ex : 6 pour un abonnement de 6 kVA)
Possibilité de ne pas afficher ces informations avec le paramètre optionnel showTarifs

Code du fichier tempo.php
<?php
$urlForecast = "https://open-dpe.fr/assets/tempo_days.json";
$urlTarifs = "https://open-dpe.fr/api/v1/electricity.php?tarif=EDF_bleu";

$jsonForecast = file_get_contents($urlForecast);
$jsonTarifs = file_get_contents($urlTarifs);

if ($jsonForecast === false || $jsonTarifs === false) {
    echo "Erreur de récupération";
} else {
    $dataForecast = json_decode($jsonForecast, true);
    $dataTarifs = json_decode($jsonTarifs, true);

    $result = [
        "previsions" => $dataForecast,
        "tarifs" => $dataTarifs['options']['tempo'] ?? null
    ];

    echo json_encode($result);
}
?>
Code du widget
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="other" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#">
  <!-- ################ nooTempo ################ 
       ********** Paramètres Optionnels ***************
		
         nbJours : Nombre de jours affichés (Défaut 9).
         puissance : Puissance kVA (Défaut 9).
         showTarifs : Afficher l'entête des tarifs (1:oui, 0:non).
         showTotal : Afficher la ligne Total Brut (1:oui, 0:non).
         showBalance : Afficher la barre Net/EnR et sa légende (1:oui, 0:non).
         showStocks : Afficher Jours restants (1:oui, 0:non).
         proportional : Affichage probabilités proportionnel (1:oui, 0:non).

        Pensez au café pour les nuits blanches de codage ;) 
        https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=noodom.fr%40gmail.com&currency_code=EUR&source=url
       ########### by @noodom ;)  ############# -->
  
  <div class="title #hide_name#">
    <div class="cmdName" style="font-weight: bold; margin-bottom: 5px;">#name_display#</div>
  </div>
  
  <div id="tempo_tarifs_header_#id#" class="t-header-tarifs"></div>

  <div class="tempo-forecast-wrapper" id="tempo_container_#id#"></div>

  <template>
      <div>
          <div>nbJours : Nombre de jours affichés (Défaut 9).</div>
          <div>puissance : Puissance kVA (Défaut 9).</div>
          <div>showTarifs : Afficher l'entête des tarifs (1:oui, 0:non).</div>
          <div>showTotal : Afficher la ligne Total Brut (1:oui, 0:non).</div>
          <div>showBalance : Afficher la barre Net/EnR (1:oui, 0:non).</div>
          <div>showStocks : Afficher Jours restants (1:oui, 0:non).</div>
          <div>proportional : Affichage probabilités proportionnel (1:oui, 0:non).</div>
      </div>
  </template>

  <style>
    .t-header-tarifs {
      background: rgba(0,0,0,0.15);
      border-radius: 8px;
      margin: 10px;
      padding: 10px;
      display: flex;
      justify-content: space-around;
      align-items: center;
      font-size: 0.9em;
      flex-wrap: wrap;
      border: 1px solid rgba(255,255,255,0.1);
    }
    .t-header-item { text-align: center; padding: 5px 10px; min-width: 100px; }
    .t-header-item b { display: block; font-size: 1.1em; }
    .t-header-label { font-size: 0.75em; font-weight: bold; text-transform: uppercase; margin-bottom: 3px; display: block; }

    /* Couleurs spécifiques pour l'écriture des tarifs */
    .txt-bleu { color: #00a8ff !important; }
    .txt-blanc { color: #f5f6fa !important; text-shadow: 0 0 2px #000; }
    .txt-rouge { color: #e84118 !important; }
    .txt-abo { color: #95afc0 !important; }

    .tempo-forecast-wrapper {
      display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; padding: 10px;
    }
    .tempo-day-card {
      display: flex; flex-direction: column; align-items: center; border-radius: 10px;
      padding: 10px; min-width: 135px; box-shadow: 0 4px 6px rgba(0,0,0,0.3);
      border: 1px solid rgba(255,255,255,0.15);
    }
    .t-date { font-size: 1em; font-weight: bold; margin-bottom: 8px; border-bottom: 1px solid rgba(0,0,0,0.1); width: 100%; text-align: center; }
    
    .t-power-zone { font-size: 0.75em; margin-bottom: 8px; width: 100%; }
    .t-power-line { display: flex; justify-content: space-between; margin-bottom: 4px; cursor: help; }
    
    .t-balance-container { margin: 8px 0; width: 100%; }
    .t-balance-bar {
      display: flex; width: 100%; height: 8px; background: rgba(0,0,0,0.1);
      border-radius: 4px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05); cursor: help;
    }
    .bar-net { background: #2d3436; height: 100%; }
    .bar-enr { background: #00b894; height: 100%; }
    
    .t-balance-legend {
      display: flex; justify-content: space-between; font-size: 0.85em; 
      margin-top: 4px; line-height: 1.1;
    }
    .leg-item { display: flex; flex-direction: column; cursor: help; }
    .leg-label { font-size: 0.75em; opacity: 0.8; margin-bottom: 1px; }
    .leg-net { font-weight: bold; text-align: left; }
    .leg-enr { font-weight: 500; text-align: right; }
    .t-arrow-down { font-size: 0.9em; font-weight: bold; margin-right: 1px; }

    .t-probas-row {
      display: flex; gap: 3px; margin-bottom: 10px; background: rgba(0,0,0,0.15);
      padding: 4px; border-radius: 6px; min-height: 28px; width:100%; box-sizing: border-box; align-items: center; justify-content: center;
    }
    .t-mini-box {
      display: inline-flex !important; align-items: center !important; justify-content: center !important;
      height: 20px !important; border-radius: 3px !important; font-size: 8px !important; font-weight: 900 !important; color: white !important;
    }
    .t-fixe { width: 20px !important; }
    .t-prop { width: 100%; }

    .t-bleu { background-color: #0055b8 !important; color: white !important; }
    .t-blanc { background-color: #f8f9fa !important; color: #1e272e !important; }
    .t-rouge { background-color: #ce0501 !important; color: white !important; }
    
    .mini-b { background-color: #003a7e !important; }
    .mini-w { background-color: #ffffff !important; color: #1e272e !important; border: 1px solid #ced4da; }
    .mini-r { background-color: #920401 !important; }

    .t-stock-footer {
      display: flex; gap: 8px; font-size: 0.75em; font-weight: bold; border-top: 1px solid rgba(0,0,0,0.1);
      padding-top: 6px; width: 100%; justify-content: space-around;
    }
  </style>

  <script>
    jeedom.cmd.update['#id#'] = function(_options) {
      try {
        let rawData = JSON.parse(_options.display_value);
        let data = rawData.previsions;
        let tarifs = rawData.tarifs;
        let container = $('#tempo_container_#id#');
        let header = $('#tempo_tarifs_header_#id#');
        
        container.empty();
        header.empty();

        let limit = (is_numeric('#nbJours#')) ? parseInt('#nbJours#') : 9;
        let pwr = (is_numeric('#puissance#')) ? '#puissance#' : "9";
        let showTarifs = ('#showTarifs#' == '0') ? false : true;
        let showTotal = ('#showTotal#' == '0') ? false : true;
        let showBalance = ('#showBalance#' == '0') ? false : true;
        let showStocks = ('#showStocks#' == '0') ? false : true;
        let isProp = ('#proportional#' == '1') ? true : false;

        // 1. Affichage du Header Tarifaire avec couleurs d'écriture
        if (!showTarifs) {
          header.hide();
        } else {
          header.show();
          if(tarifs) {
            let abo = (tarifs.abonnement && tarifs.abonnement.puissance_kVA && tarifs.abonnement.puissance_kVA[pwr]) ? tarifs.abonnement.puissance_kVA[pwr] : "?";
            let p = tarifs.prix_kWh;
            
            let h_html = `<div class="t-header-item"><span class="t-header-label txt-abo">Abo (${pwr}kVA)</span><b>${abo}€/m</b></div>`;
            
            if (p) {
              h_html += `
                <div class="t-header-item"><span class="t-header-label txt-bleu">Bleu (HP/HC)</span><b class="txt-bleu">${p.bleu.HP} / ${p.bleu.HC}</b></div>
                <div class="t-header-item"><span class="t-header-label txt-blanc">Blanc (HP/HC)</span><b class="txt-blanc">${p.blanc.HP} / ${p.blanc.HC}</b></div>
                <div class="t-header-item"><span class="t-header-label txt-rouge">Rouge (HP/HC)</span><b class="txt-rouge">${p.rouge.HP} / ${p.rouge.HC}</b></div>
              `;
            }
            header.append(h_html);
          }
        }

        // 2. Affichage des tuiles
        data.slice(0, limit).forEach(item => {
          let dateParts = item.date.split('-');
          let shortDate = dateParts[2] + '/' + dateParts[1];
          
          let f_gross = (item.forecast / 1000).toFixed(1);
          let f_net = (item.consumption_net / 1000).toFixed(1);
          let f_enr = (f_gross - f_net).toFixed(1);
          
          let pctNet = (item.consumption_net / item.forecast * 100).toFixed(0);
          let pctEnR = (100 - pctNet).toFixed(0);

          let html = `<div class="tempo-day-card t-${item.tempo_color.toLowerCase()}">`;
          html += `<span class="t-date">${shortDate}</span>`;
          
          if (showTotal || showBalance) {
            html += `<div class="t-power-zone">`;
            
            // PARAMETRE 1 : LIGNE TOTAL
            if (showTotal) {
              html += `<div class="t-power-line" title="Consommation brute totale prévue sur le réseau (en Gigawatts)"><span>Total:</span><b>${f_gross} GW</b></div>`;
            }
            
            // PARAMETRE 2 : BARRE + NET + ENR
            if (showBalance) {
              html += `<div class="t-balance-container">
                        <div class="t-balance-bar" title="Répartition : ${pctNet}% Pilotable / ${pctEnR}% Renouvelable">
                          <div class="bar-net" style="width:${pctNet}%"></div>
                          <div class="bar-enr" style="width:${pctEnR}%"></div>
                        </div>
                        <div class="t-balance-legend">
                          <div class="leg-item" title="Consommation NETTE : effort réel demandé aux centrales pilotables (Nucléaire, Gaz, etc.)">
                             <span class="leg-label">Net</span>
                             <span class="leg-net">${f_net}G</span>
                          </div>
                          <div class="leg-item" title="Apport EnR : part de la consommation couverte par le Solaire et l'Éolien (déduite du Total)">
                             <span class="leg-label">EnR</span>
                             <span class="leg-enr"><span class="t-arrow-down">↓</span>${f_enr}G</span>
                          </div>
                        </div>
                      </div>`;
            }
            html += `</div>`;
          }

          html += `<div class="t-probas-row ${isProp ? 't-prop' : ''}">`;
          const probas = [
            {v:Math.round(item.probability_bleu * 100),c:'mini-b',t:'Bleu'},
            {v:Math.round(item.probability_blanc * 100),c:'mini-w',t:'Blanc'},
            {v:Math.round(item.probability_rouge * 100),c:'mini-r',t:'Rouge'}
          ];
          probas.forEach(p => {
            if (isProp) {
              if (p.v > 0) html += `<div class="t-mini-box ${p.c}" style="flex:${p.v};margin:0 1px;" title="Probabilité ${p.t}: ${p.v}%">${p.v}</div>`;
            } else {
              html += `<div class="t-mini-box ${p.c} t-fixe" style="margin:0 2px;" title="Probabilité ${p.t}: ${p.v}%">${p.v}</div>`;
            }
          });
          html += `</div>`;

          if (showStocks) {
            html += `<div class="t-stock-footer">
                      <div title="Jours Blancs restants dans la saison (Total 43)">⚪ ${item.stock_blanc}</div>
                      <div title="Jours Rouges restants dans la saison (Total 22)">🔴 ${item.stock_rouge}</div>
                    </div>`;
          }

          html += `</div>`;
          container.append(html);
        });
      } catch (e) {
        $('#tempo_container_#id#').html('Erreur JSON');
      }
    }
    jeedom.cmd.update['#id#']({display_value:'#state#'});
  </script>
</div>