Accéder aux variables depuis un HTML sur un design

Bonjour à tous !

Après avoir vu le post sur l’utilisation de plugin-htmldisplay pour afficher sur un design un graphique, je m’en suis inspiré pour fabriquer le mien.
Ayant un système elec Enphase, j’apprécie beaucoup la présentation des puissances/énergies sur leur site :


J’entreprends de refaire ce graph chez moi :

Le code dans mon équipement plugin-htmldisplay :

<div id="controls" style="margin-bottom: 1em;">
  <label><input type="radio" name="duree" value="1" checked> 24h glissant</label>
  <label><input type="radio" name="duree" value="2"> Depuis 5h (veille)</label>
  &nbsp;|&nbsp;
  <button onclick="changerStack(-1)">−</button>
  <span id="stackWidthDisplay">2</span> min
  <button onclick="changerStack(1)">+</button>
</div>

<div id="container" style="height:400px;"></div>

<script>
var duree = typeof duree !== 'undefined' ? duree : 1;
var stackWidth = typeof stackWidth !== 'undefined' ? stackWidth : 2;
var stackSteps = stackSteps || [2, 5, 10, 15, 20, 30, 60];

  
document.querySelectorAll('input[name="duree"]').forEach(radio => {
  radio.addEventListener('change', e => {
    duree = parseInt(e.target.value);
    chargerDonnees();
  });
});

function changerStack(direction) {
  const currentIndex = stackSteps.indexOf(stackWidth);
  const nextIndex = currentIndex + direction;
  if (nextIndex >= 0 && nextIndex < stackSteps.length) {
    stackWidth = stackSteps[nextIndex];
    document.getElementById('stackWidthDisplay').textContent = stackWidth;
    chargerDonnees();
  }
}
function chargerDonnees() {
  //const duree = 1; //1 = 24h glissant, 2= depuis la veille à 5h
  //const stackWidth = 2; //pour regrouper par 5min, 10 min, 15 minutes etc...
  const now = new Date(); // heure locale
  const endTime = now.getTime(); // maintenant, en local → timestamp (UTC millisecondes)

  let startTime;
  if (duree == 1) {
    // Glissant 24h
    startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).getTime();
  } else {
    // De 5h la veille jusqu'à maintenant
    startTime = new Date(
      now.getFullYear(),
      now.getMonth(),
      now.getDate() - 1, // ← veille !
      5, 0, 0, 0
    ).getTime();
  }

  const puissances = {
    prod: new Map(),
    cons: new Map(),
    exp: new Map(),
    imp: new Map()
  };

  const arrondiXmin = timestamp => Math.floor(timestamp / (stackWidth * 60 * 1000)) * (stackWidth * 60 * 1000);

  let chargements = 0;

  const noms = {
    5098: { map: puissances.prod, facteur: 1 },
    5104: { map: puissances.cons, facteur: -1 },
    5115: { map: puissances.exp, facteur: -1 },
    5116: { map: puissances.imp, facteur: 1 }
  };

  Object.entries(noms).forEach(([cmd_id, obj]) => {
    jeedom.history.get({
      cmd_id: parseInt(cmd_id),
      dateStart: new Date(startTime).toISOString().slice(0, 19).replace('T', ' '),
      dateEnd: new Date(endTime).toISOString().slice(0, 19).replace('T', ' '),
      success: function (result) {
        result.data.forEach(entry => {
          const t = arrondiXmin(entry[0]);
          //if (t < startTime || t > endTime) return; // filtre sécurité
          const val = obj.facteur * entry[1];
          const list = obj.map.get(t) || [];
          list.push(val);
          obj.map.set(t, list);
        });
        chargements++;
        if (chargements === 4) dessinerGraphique();
      }
    });
  });

  
  function dessinerGraphique() {
    const allTimestamps = Array.from(
      new Set([
        ...puissances.prod.keys(),
        ...puissances.cons.keys(),
        ...puissances.exp.keys(),
        ...puissances.imp.keys()
      ])
    ).sort((a, b) => a - b);

    const moy = arr => arr && arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;

    const serie_prod = [], serie_imp = [], serie_cons = [], serie_exp = [];

    allTimestamps.forEach(ts => {
      serie_prod.push([ts, Math.trunc(moy(puissances.prod.get(ts)))]);
      serie_imp.push([ts, Math.trunc(moy(puissances.imp.get(ts)))]);
      serie_cons.push([ts, Math.trunc(moy(puissances.cons.get(ts)))]);
      serie_exp.push([ts, Math.trunc(moy(puissances.exp.get(ts)))]);
    });

    Highcharts.setOptions({
      global: {
    	useUTC: false
      },
      lang: {
        months: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin',
          'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
        weekdays: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
        shortMonths: ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
          'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.']
      }
    });

    Highcharts.chart('container', {
      chart: { type: 'column' },
      title: { text: null, align: 'left' },
      xAxis: {
        type: 'datetime',
        min: startTime,
        max: endTime,
        tickInterval: 60 * 60 * 1000, // 1 heure
        labels: {
          format: '{value:%H}h'
        }
      },
      yAxis: {
        title: { text: 'Puissance (W)' },
        labels: {
    		formatter: function () {
      			return Math.abs(this.value);
    		}
  		},
        stackLabels: { enabled: false }
      },
      tooltip: {
  		xDateFormat: '%H:%M',
  		shared: true,
  		formatter: function () {
          	const pointStart = this.x;
    		const pointEnd = pointStart + stackWidth * 60 * 1000; // en ms
    		const formatHeure = ts => Highcharts.dateFormat('%H:%M', ts);
    		let s = `<b>${formatHeure(pointStart)} → ${formatHeure(pointEnd)}</b><br/>`;
    		//let s = `<b>${Highcharts.dateFormat('%H:%M', this.x)}</b><br/>`;
    		this.points.forEach(point => {
      		const val = Math.abs(point.y);
              if (val !== 0) {
        		s += `<span style="color:${point.series.color}">●</span> ${point.series.name}: <b>${Math.trunc(val)}</b><br/>`;
      		}
    		});
    		return s;
  		}
      },
      plotOptions: {
        column: {
          stacking: 'normal',
          groupPadding: 0.1, // ≈ 90% de largeur
          pointPadding: 0,
          borderWidth: 0
        }
      },
      series: [
        {
          name: 'Exportation',
          data: serie_exp,
          stack: 'Puissance',
          color: '#6c7073'
        },
        {
          name: 'Importation',
          data: serie_imp,
          stack: 'Puissance',
          color: '#6c7073'
        },
        {
          name: 'Production',
          data: serie_prod,
          stack: 'Puissance',
          color: '#01b4de'
        },
        {
          name: 'Consommation',
          data: serie_cons,
          stack: 'Puissance',
          color: '#f37320'
        },
      ]
    });
  }
}

// Initialisation après chargement
function attendreEtCharger() {
  if (!document.getElementById('container')) {
    setTimeout(attendreEtCharger, 100);
    return;
  }
  chargerDonnees();
}
attendreEtCharger();
</script>

Comme vous pouvez le voir, j’ai deux commandes en haut du visuel qui permettent de modifier la durée à afficher entre deux choix (variable « duree »), et modifier la largeurs des barres parmi : 2, 5, 10, 15, 20, 30, 60 minutes (variable « stackWidth »).

Ca marche mais à chaque fois que je recharge la page ça repart vers une valeur par défaut.

Idéalement j’aimerais que mes deux informations duree et stackWidth soient lues dans les variables Jeedom et que ces variables soient réécrites à chaque fois qu’on modifie les paramètres.
J’ai demandé de l’aide à mon pote ChatGPT et Mistral mais je pense qu’il ne connaissent pas précisément les possibilités de jeedom.

Ils me propose d’utiliser des méthode jeedom.var.get() et jeedom.var.save() mais ça ne semble pas exister.
La partie du code modifiée et la proposition qui ne fonctionne pas :

  let nbOK = 0;

  jeedom.var.get({
    name: 'graph_duree',
    success: function (val) {
      if (val !== null) {
        duree = parseInt(val);
        document.querySelector(`input[name="duree"][value="${duree}"]`).checked = true;
      }
      if (++nbOK === 2) chargerDonnees();
    }
  });

  jeedom.var.get({
    name: 'graph_stackWidth',
    success: function (val) {
      if (val !== null) {
        stackWidth = parseInt(val);
      }
      document.getElementById('stackWidthDisplay').textContent = stackWidth;
      if (++nbOK === 2) chargerDonnees();
    }
  });

  // Ajouter les écouteurs sur les boutons radio
  document.querySelectorAll('input[name="duree"]').forEach(radio => {
    radio.addEventListener('change', function () {
      duree = parseInt(this.value);
      jeedom.var.save({ name: 'graph_duree', value: duree });
      chargerDonnees();
    });
  });
}

function changerStack(direction) {
  const index = stackSteps.indexOf(stackWidth);
  const newIndex = Math.max(0, Math.min(stackSteps.length - 1, index + direction));
  if (newIndex !== index) {
    stackWidth = stackSteps[newIndex];
    document.getElementById('stackWidthDisplay').textContent = stackWidth;
    jeedom.var.save({ name: 'graph_stackWidth', value: stackWidth });
    chargerDonnees();
  }
}

J’aurais voulu savoir dans la communauté si quelqu’un savait comment faire pour accéder aux variables depuis du html affiché sur un design, un peu comme on connait les commande php depuis un scenario :

$scenario->setData($key, $value); : Sauvegarde une donnée (variable).
     $key : clé de la valeur (int ou string).
     $value : valeur à stocker (int, string, array ou object).
$scenario->getData($key); : Récupère une donnée (variable).
     $key => 1 : clé de la valeur (int ou string).

Merci à tous !

PS :

Je remercie tous ceux qui ont pris le temps de lire et s’intéresser à mon problème ! J’ai vu qu’il y avait eu quand même pas mal de lecture.
Après un peu de patience, la compréhension des fonctions getData et setData qui utilisent la classe dataStore, j’ai finalement réussi à faire ce que je voulais.
Je vous le présente :

Le visuel HTML de mon graphique va envoyer des appel AJAX à un script php perso qui va faire appel à cette classe qui permet de lire et écrire dans les variables :

J’ai place ce script variableGetSet.php dans ./html/script de jeedom :

<?php
require_once __DIR__ . '/../core/php/core.inc.php';

header('Content-Type: application/json');

$action = isset($_GET['action']) ? $_GET['action'] : '';
$key = isset($_GET['key']) ? $_GET['key'] : '';
$value = isset($_GET['value']) ? $_GET['value'] : '';

try {
    if ($action == 'set') {
        setDataStoreValue($key, $value);
        echo json_encode(['success' => true]);
    } elseif ($action == 'get') {
        $data = getDataStoreValue($key);
        echo json_encode(['success' => true, 'value' => $data]);
    } else {
        throw new Exception('Action non valide');
    }
} catch (Exception $e) {
    echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

function setDataStoreValue($key, $value, $type = 'scenario', $link_id = -1) {
    $dataStore = new dataStore();
    $dataStore->setType($type);
    $dataStore->setLink_id($link_id);
    $dataStore->setKey($key);
    $dataStore->setValue($value);
    $dataStore->save();
}

function getDataStoreValue($key, $type = 'scenario', $link_id = -1, $default = '') {
    $dataStore = dataStore::byTypeLinkIdKey($type, $link_id, $key);
    if (is_object($dataStore)) {
        return $dataStore->getValue($default);
    }
    return $default;
}
?>

Voici ensuite le code de mon équipement plugin-htmldisplay :

<div style="margin-bottom: 10px;">
  <label>
    <input type="radio" name="duree" value="1"> Glissant 24h
  </label>
  <label style="margin-left: 10px;">
    <input type="radio" name="duree" value="2"> Depuis la veille à 5h
  </label>
  <span style="margin-left: 30px;">
    Regroupement :
    <button onclick="changerStack(-1)">−</button>
    <span id="stackWidthDisplay">?</span> min
    <button onclick="changerStack(1)">+</button>
  </span>
</div>

<div id="container" style="height:400px;"></div>

<script>
var stackSteps = [2, 5, 10, 15, 20, 30, 60];
var duree = 1;
var stackWidth = 2;

// Fonction pour récupérer une valeur
function getData(key, callback) {
    const url = `/../script/variableGetSet.php?action=get&key=${encodeURIComponent(key)}`;

    fetch(url)
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                callback(data.value);
            } else {
                console.error('Erreur lors de la récupération de la donnée:', data.error);
            }
        })
        .catch(error => console.error('Erreur:', error));
}

// Fonction pour sauvegarder une valeur
function setData(key, value) {
    const url = `/../script/variableGetSet.php?action=set&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`;

    fetch(url)
        .then(response => response.json())
        .then(data => {
            if (!data.success) {
                console.error('Erreur lors de la sauvegarde de la donnée:', data.error);
            }
        })
        .catch(error => console.error('Erreur:', error));
}
  
function chargerParametresEtDonnées() {

let nbOK = 0;

// Récupérer graph_duree
getData('graph_duree', function(val) {
    if (val !== null) {
        duree = parseInt(val);
        document.querySelector(`input[name="duree"][value="${duree}"]`).checked = true;
    }
    if (++nbOK === 2) chargerDonnees();
});

// Récupérer graph_stackWidth
getData('graph_stackWidth', function(val) {
    if (val !== null) {
        stackWidth = parseInt(val);
    }
    document.getElementById('stackWidthDisplay').textContent = stackWidth;
    if (++nbOK === 2) chargerDonnees();
});

// Ajouter les écouteurs sur les boutons radio
document.querySelectorAll('input[name="duree"]').forEach(radio => {
    radio.addEventListener('change', function() {
        duree = parseInt(this.value);
        setData('graph_duree', duree);
        chargerDonnees();
    });
});
}

function changerStack(direction) {
    const index = stackSteps.indexOf(stackWidth);
    const newIndex = Math.max(0, Math.min(stackSteps.length - 1, index + direction));
    if (newIndex !== index) {
        stackWidth = stackSteps[newIndex];
        document.getElementById('stackWidthDisplay').textContent = stackWidth;
        setData('graph_stackWidth', stackWidth);
        chargerDonnees();
    }
}

function chargerDonnees() {
  const now = new Date();
  const endTime = now.getTime();
  let startTime;

  if (duree === 1) {
    startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).getTime();
  } else {
    startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 5, 0, 0, 0).getTime();
  }

  const puissances = {
    prod: new Map(),
    cons: new Map(),
    exp: new Map(),
    imp: new Map()
  };

  const arrondiXmin = timestamp => Math.floor(timestamp / (stackWidth * 60 * 1000)) * (stackWidth * 60 * 1000);
  let chargements = 0;

  const noms = {
    5098: { map: puissances.prod, facteur: 1 },
    5104: { map: puissances.cons, facteur: -1 },
    5115: { map: puissances.exp, facteur: -1 },
    5116: { map: puissances.imp, facteur: 1 }
  };

  Object.entries(noms).forEach(([cmd_id, obj]) => {
    jeedom.history.get({
      cmd_id: parseInt(cmd_id),
      dateStart: new Date(startTime).toISOString().slice(0, 19).replace('T', ' '),
      dateEnd: new Date(endTime).toISOString().slice(0, 19).replace('T', ' '),
      success: function (result) {
        result.data.forEach(entry => {
          const t = arrondiXmin(entry[0]);
          const val = obj.facteur * entry[1];
          const list = obj.map.get(t) || [];
          list.push(val);
          obj.map.set(t, list);
        });
        chargements++;
        if (chargements === 4) dessinerGraphique(startTime, endTime, puissances);
      }
    });
  });
}

function dessinerGraphique(startTime, endTime, puissances) {
  const allTimestamps = Array.from(
    new Set([
      ...puissances.prod.keys(),
      ...puissances.cons.keys(),
      ...puissances.exp.keys(),
      ...puissances.imp.keys()
    ])
  ).sort((a, b) => a - b);

  const moy = arr => arr && arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;

  const serie_prod = [], serie_imp = [], serie_cons = [], serie_exp = [];

  allTimestamps.forEach(ts => {
    serie_prod.push([ts, Math.trunc(moy(puissances.prod.get(ts)))]);
    serie_imp.push([ts, Math.trunc(moy(puissances.imp.get(ts)))]);
    serie_cons.push([ts, Math.trunc(moy(puissances.cons.get(ts)))]);
    serie_exp.push([ts, Math.trunc(moy(puissances.exp.get(ts)))]);
  });

  Highcharts.setOptions({
    global: { useUTC: false },
    lang: {
      months: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin',
        'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
      weekdays: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
      shortMonths: ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
        'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.']
    }
  });

  Highcharts.chart('container', {
    chart: { type: 'column' },
    title: { text: null, align: 'left' },
    xAxis: {
      type: 'datetime',
      min: startTime,
      max: endTime,
      tickInterval: 60 * 60 * 1000,
      labels: { format: '{value:%H}h' }
    },
    yAxis: {
      title: { text: 'Puissance (W)' },
      labels: {
        formatter: function () {
          return Math.abs(this.value);
        }
      },
      stackLabels: { enabled: false }
    },
    tooltip: {
      xDateFormat: '%H:%M',
      shared: true,
      formatter: function () {
        const pointStart = this.x;
        const pointEnd = pointStart + stackWidth * 60 * 1000;
        const formatHeure = ts => Highcharts.dateFormat('%H:%M', ts);
        let s = `<b>${formatHeure(pointStart)} → ${formatHeure(pointEnd)}</b><br/>`;
        this.points.forEach(point => {
          const val = Math.abs(point.y);
          if (val !== 0) {
            s += `<span style="color:${point.series.color}">●</span> ${point.series.name}: <b>${Math.trunc(val)}</b><br/>`;
          }
        });
        return s;
      }
    },
    plotOptions: {
      column: {
        stacking: 'normal',
        groupPadding: 0.1,
        pointPadding: 0,
        borderWidth: 0
      }
    },
    series: [
      {
        name: 'Exportation',
        data: serie_exp,
        stack: 'Puissance',
        color: '#6c7073'
      },
      {
        name: 'Importation',
        data: serie_imp,
        stack: 'Puissance',
        color: '#6c7073'
      },
      {
        name: 'Production',
        data: serie_prod,
        stack: 'Puissance',
        color: '#01b4de'
      },
      {
        name: 'Consommation',
        data: serie_cons,
        stack: 'Puissance',
        color: '#f37320'
      },
    ]
  });
}

// Démarrage après chargement du DOM
function attendreEtCharger() {
  if (!document.getElementById('container')) {
    setTimeout(attendreEtCharger, 100);
    return;
  }
  chargerParametresEtDonnées();
}
attendreEtCharger();
</script>

Et voici le rendu final, avec les settings qui s’enregistrent dans les variables graph_duree et graph_stackWidth :

L’utilité est discutable mais ça pourrait être utile pour d’autres usages alors je partage !

6 « J'aime »

Bonsoir

Très intéressant pour ceux qui suive leurs productions solaires et son utilisation.
J’aimerai utiliser ton Dev.
Comment/où renseigner les id des infos à utiliser pour tracer le graphique ?

Merci de ton aide

Hello,

C’est dans le code du htmldisplay :

  const noms = {
    5098: { map: puissances.prod, facteur: 1 },
    5104: { map: puissances.cons, facteur: -1 },
    5115: { map: puissances.exp, facteur: -1 },
    5116: { map: puissances.imp, facteur: 1 }
  };

Ce sont les id des commandes correspondantes.
Depuis j’ai un peu modifié mes codes, notamment car j’avais une erreur entr el’heure UTC et l’heure de Paris.
Et aussi car là où j’avais mis initialement mon script, Jeedom le supprimais sans rien me dire de temps en temps. Donc je l’ai déplacé dans html/plugins/scrpit/data
Je les remets ici :

  • le script : html/plugins/scrpit/data/variableGetSet.php:
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

  require_once __DIR__ . '/../../../core/php/core.inc.php';//ATTENTION si placé en html/script il faut mettre '/../core/php/core.inc.php'. Si rangé en html/plugins/script/data, il faut mettre 

header('Content-Type: application/json');

$action = isset($_GET['action']) ? $_GET['action'] : '';
$key = isset($_GET['key']) ? $_GET['key'] : '';
$value = isset($_GET['value']) ? $_GET['value'] : '';

try {
    if ($action == 'set') {
        setDataStoreValue($key, $value);
        echo json_encode(['success' => true]);
    } elseif ($action == 'get') {
        $data = getDataStoreValue($key);
        echo json_encode(['success' => true, 'value' => $data]);
    } else {
        throw new Exception('Action non valide');
    }
} catch (Exception $e) {
    echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

function setDataStoreValue($key, $value, $type = 'scenario', $link_id = -1) {
    $dataStore = new dataStore();
    $dataStore->setType($type);
    $dataStore->setLink_id($link_id);
    $dataStore->setKey($key);
    $dataStore->setValue($value);
    $dataStore->save();
}

function getDataStoreValue($key, $type = 'scenario', $link_id = -1, $default = '') {
    $dataStore = dataStore::byTypeLinkIdKey($type, $link_id, $key);
    if (is_object($dataStore)) {
        return $dataStore->getValue($default);
    }
    return $default;
}
?>

Le code du htmldisplay:

<div style="margin-right: 20px; text-align: right;">
  <label>
    <input type="radio" name="duree" value="1"> 24h
  </label>
  <label style="margin-left: 10px;">
    <input type="radio" name="duree" value="2"> J-1 à 5h
  </label>
  <span style="margin-left: 30px;">
    <button onclick="changerStack(-1)">−</button>
    <span id="stackWidthDisplay">15</span> min
    <button onclick="changerStack(1)">+</button>
  </span>
</div>

<div id="container" style="height:350px;"></div>

<script>
var stackSteps = [2, 5, 10, 15, 20, 30, 60];
var duree = 1;
var stackWidth = 2;

// Fonction pour récupérer une valeur depuis Jeedom
function getData(key, callback) {
    const url = `../plugins/script/data/variableGetSet.php?action=get&key=${encodeURIComponent(key)}`;
    fetch(url)
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                callback(data.value);
            } else {
                console.error('Erreur lors de la récupération de la donnée:', data.error);
            }
        })
        .catch(error => console.error('Erreur:', error));
}

// Fonction pour sauvegarder une valeur dans Jeedom
function setData(key, value) {
    const url = `../plugins/script/data/variableGetSet.php?action=set&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`;
    fetch(url)
        .then(response => response.json())
        .then(data => {
            if (!data.success) {
                console.error('Erreur lors de la sauvegarde de la donnée:', data.error);
            }
        })
        .catch(error => console.error('Erreur:', error));
}

// Formate les dates au format local pour Jeedom → "YYYY-MM-DD HH:MM:SS"
function formatDateJeedom(ts) {
    const d = new Date(ts);
    const pad = n => String(n).padStart(2, '0');
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

function chargerParametresEtDonnées() {
    let nbOK = 0;

    // Récupérer graph_duree
    getData('graph_duree', function(val) {
        if (val !== null) {
            duree = parseInt(val);
            document.querySelector(`input[name="duree"][value="${duree}"]`).checked = true;
        }
        if (++nbOK === 2) chargerDonnees();
    });

    // Récupérer graph_stackWidth
    getData('graph_stackWidth', function(val) {
        if (val !== null) {
            stackWidth = parseInt(val);
        }
        document.getElementById('stackWidthDisplay').textContent = stackWidth;
        if (++nbOK === 2) chargerDonnees();
    });

    // Ajout des écouteurs sur les boutons radio
    document.querySelectorAll('input[name="duree"]').forEach(radio => {
        radio.addEventListener('change', function() {
            duree = parseInt(this.value);
            setData('graph_duree', duree);
            chargerDonnees();
        });
    });
}

function changerStack(direction) {
    const index = stackSteps.indexOf(stackWidth);
    const newIndex = Math.max(0, Math.min(stackSteps.length - 1, index + direction));
    if (newIndex !== index) {
        stackWidth = stackSteps[newIndex];
        document.getElementById('stackWidthDisplay').textContent = stackWidth;
        setData('graph_stackWidth', stackWidth);
        chargerDonnees();
    }
}

function chargerDonnees() {
    const now = new Date();
    const endTime = now.getTime();
    let startTime;

    if (duree === 1) {
        startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).getTime();
    } else {
        startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 5, 0, 0, 0).getTime();
    }

    const puissances = {
        prod: new Map(),
        cons: new Map(),
        exp: new Map(),
        imp: new Map()
    };

    const arrondiXmin = timestamp => Math.floor(timestamp / (stackWidth * 60 * 1000)) * (stackWidth * 60 * 1000);
    let chargements = 0;

    const noms = {
        1070: { map: puissances.prod, facteur: 1 },
        5104: { map: puissances.cons, facteur: -1 },
        5115: { map: puissances.exp, facteur: -1 },
        5116: { map: puissances.imp, facteur: 1 }
    };

  Object.entries(noms).forEach(([cmd_id, obj]) => {
        jeedom.history.get({
            cmd_id: parseInt(cmd_id),
            dateStart: formatDateJeedom(startTime),
            dateEnd: formatDateJeedom(endTime),
            success: function (result) {
                result.data.forEach(entry => {
                    const t = arrondiXmin(entry[0] + new Date().getTimezoneOffset() * 60000);
                    const val = obj.facteur * entry[1];
                    const bucket = obj.map.get(t) || { sum: 0, count: 0 };
                    bucket.sum += val;
                    bucket.count++;
                    obj.map.set(t, bucket);
                });
                chargements++;
                if (chargements === 4) dessinerGraphique(startTime, endTime, puissances);
            }
        });
    });
}

function dessinerGraphique(startTime, endTime, puissances) {
    const allTimestamps = Array.from(
        new Set([
            ...puissances.prod.keys(),
            ...puissances.cons.keys(),
            ...puissances.exp.keys(),
            ...puissances.imp.keys()
        ])
    ).sort((a, b) => a - b);

    const getAvg = bucket => (bucket && bucket.count) ? bucket.sum / bucket.count : 0;

    const serie_prod = [], serie_imp = [], serie_cons = [], serie_exp = [];

    allTimestamps.forEach(ts => {
        const avgProd = getAvg(puissances.prod.get(ts));
        const avgImp  = getAvg(puissances.imp.get(ts));
        const avgCons = getAvg(puissances.cons.get(ts));
        const avgExp  = getAvg(puissances.exp.get(ts));
        // Vérifier bilan et corriger l'export pour forcer la conservation physique
        // somme signée : prod(+), imp(+), cons(-), exp(-) -> net = prod+imp+cons+exp ; doit être ~0
        const net = avgProd + avgImp + avgCons + avgExp;
        let correctedExp = avgExp;
        if (Math.abs(net) > 0.5) { // seuil 0.5W pour éviter micro-ajustements
            correctedExp = avgExp - net; // corrige pour ramener net à 0
            // optionnel : console.log(`Correction bucket ${new Date(ts).toISOString()}: net=${net.toFixed(2)}, exp->${correctedExp.toFixed(2)}`);
        }
        
        // garder valeurs signées pour le stacking (les exports/consommations négatives vont vers le bas)
        serie_prod.push([ts, Math.round(avgProd)]);
        serie_imp.push([ts, Math.round(avgImp)]);
        serie_cons.push([ts, Math.round(avgCons)]);
        serie_exp.push([ts, Math.round(correctedExp)]);
    });

    Highcharts.setOptions({
        global: { useUTC: false }, // Force Highcharts à utiliser l'heure locale
        lang: {
            months: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin',
                'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
            weekdays: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
            shortMonths: ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin',
                'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.']
        }
    });

    Highcharts.chart('container', {
        chart: { type: 'column' },
        title: { text: null, align: 'left' },
        xAxis: {
            type: 'datetime',
            min: startTime,
            max: endTime,
            tickInterval: 60 * 60 * 1000,
            labels: { format: '{value:%H}h' }
        },
        yAxis: {
            visible: false,
            min: -2600,
            max: 2600,
            startOnTick: false,
            endOnTick: false,
            title: { text: 'Puissance (W)' },
            labels: {
                formatter: function () {
                    return Math.abs(this.value);
                }
            },
            stackLabels: { enabled: false }
        },
        tooltip: {
            xDateFormat: '%H:%M',
            shared: true,
            formatter: function () {
                const pointStart = this.x;
                const pointEnd = pointStart + stackWidth * 60 * 1000;
                const formatHeure = ts => Highcharts.dateFormat('%H:%M', ts);
                let s = `<b>${formatHeure(pointStart)} → ${formatHeure(pointEnd)}</b><br/>`;
                this.points.forEach(point => {
                    const val = Math.abs(point.y);
                    if (val !== 0) {
                        s += `<span style="color:${point.series.color}">●</span> ${point.series.name}: <b>${Math.trunc(val)}</b><br/>`;
                    }
                });
                return s;
            }
        },
        plotOptions: {
            column: {
                stacking: 'normal',
                groupPadding: 0.1,
                pointPadding: 0,
                borderWidth: 0
            }
        },
        series: [
            {
                name: 'Exportation',
                data: serie_exp,
                stack: 'Puissance',
                color: '#6c7073'
            },
            {
                name: 'Importation',
                data: serie_imp,
                stack: 'Puissance',
                color: '#6c7073'
            },
            {
                name: 'Production',
                data: serie_prod,
                stack: 'Puissance',
                color: '#01b4de'
            },
            {
                name: 'Consommation',
                data: serie_cons,
                stack: 'Puissance',
                color: '#f37320'
            },
        ]
    });
}

// Démarrage après chargement du DOM
function attendreEtCharger() {
    if (!document.getElementById('container')) {
        setTimeout(attendreEtCharger, 100);
        return;
    }
    chargerParametresEtDonnées();
}
attendreEtCharger();
</script>

1 « J'aime »

Top merci !

Je ne comprends pas bien la phrase : " ```
require_once DIR . ‹ /…/…/…/core/php/core.inc.php ›;//ATTENTION si placé en html/script il faut mettre ‹ /…/core/php/core.inc.php ›. Si rangé en html/plugins/script/data, il faut mettre


... mais tout fonctionne !
Merci

Ah oui c’est parce qu’avant le fichier variableGetSet.php étant placé dans html/script, et j’ai dû le déplacé dans html/plugins.script/data car il disparaissait parfois sans prévenir.
Et alors le chemin vers le fichier core.inc.php est différent. Je m’étais laissé l’ancien dans le commentaire au cas où.

1 « J'aime »

Bonjour

Encore mille mercis à @TonioBDS

J’ai fait travaillé GPT pour transformer le HTML en widget pour permettre de lui passer des paramètres. Le graph n’a absolument pas changé, il est parfait pour moi.
Le titre, les noms des valeurs, les sources des valeurs et les couleurs sont maintenant des paramètres.

La mise en oeuvre :

  • le script ne change pas : html/plugins/scrpit/data/variableGetSet.php
  • la création d’un widget info numérique (cmd.info.numeric.graph-electricite.html) dans le répertoire data\customTemplates. Coller le code ci-dessous.
  • la création d’un virtuel qui ne comporte qu’une seule info et qui sert uniquement de support pour passer les paramètres au graphique.

Il n’y a pas de paramètre pour le fond du graphique car elle peut être changée dans le design directement par clique droit sur l’équipement « Paramétres d’affichages ». Idem pour ajouter une bordure arrondie autour du graphique.

<template>
  <!-- Définition des paramètres configurables du widget avec leurs valeurs par défaut -->
  <div>title : Titre du graphique. [ défaut : "Electricité" ]</div>
  <div>defaultDuree : Durée par défaut. 1 = 24h, 2 = J-1 à 5h. [ défaut : 2 ]</div>
  <div>stackWidth : Largeur du stack en minutes. [ défaut : 15 ]</div>
  <div>idProduction : ID de la commande Production. [ obligatoire ]</div>
  <div>idConso : ID de la commande Consommation. [ obligatoire ]</div>
  <div>idExport : ID de la commande Exportation. [ obligatoire ]</div>
  <div>idBatterie : ID de la commande Batterie. [ obligatoire ]</div>
  <div>nameProduction : Nom de la courbe Production. [ défaut : "Production solaire" ]</div>
  <div>nameConso : Nom de la courbe Consommation. [ défaut : "Consommation globale" ]</div>
  <div>nameExport : Nom de la courbe Exportation. [ défaut : "Utilisation réseau" ]</div>
  <div>nameBatterie : Nom de la courbe Batterie. [ défaut : "Utilisation batterie" ]</div>
  <div>colorProduction : Couleur de la courbe Production. [ défaut : "#01b4de" ]</div>
  <div>colorConso : Couleur de la courbe Consommation. [ défaut : "#f37320" ]</div>
  <div>colorExport : Couleur de la courbe Exportation. [ défaut : "#6c7073" ]</div>
  <div>colorBatterie : Couleur de la courbe Batterie. [ défaut : "#008000" ]</div>
  <div>titleColor : Couleur du titre et des commandes. [ défaut : "#ffffff" ]</div>
  <div>headerBgColor : Couleur de fond du bandeau supérieur. [ défaut : "rgba(0,0,0,0.4)" ]</div>
</template>

<style>
/* Style des boutons radio */
input[type="radio"] {
  accent-color: #01b4de; /* couleur par défaut du thème */
  transform: scale(1.2);
  margin-right: 5px;
}

/* Style des boutons + et - */
button[id^="btn"] {
	background-color: #01b4de;
  color: white;
  border: none;
  border-radius: 50%!important; /* rend le bouton rond */
  width: 28px;
  height: 28px;
  cursor: pointer;
  font-size: 16px;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease;
}

button[id^="btn"]:hover {
  background-color: #028bb0;
}

button[id^="btn"]:active {
  background-color: #016f8b;
}
</style>

<div id="graphContainer_#id#" style="width:100%;height:100%;min-height:300px;position:relative;">
  <!-- Bandeau supérieur du widget : contient le titre et les commandes -->
  <div id="graphHeader_#id#" style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;box-sizing:border-box;border-radius:6px;flex-wrap:wrap;">
    <!-- Titre du graphique -->
    <div id="graphTitle_#id#" style="text-align:left;font-size:18px;margin-left:10px;">#title#</div>
    <!-- Section des commandes (radio pour durée et boutons pour stackWidth) -->
    <div style="display:flex;align-items:center;gap:15px;">
      <div>
        <label><input type="radio" name="duree_#id#" value="1"> 24h</label>
        <label style="margin-left:10px;"><input type="radio" name="duree_#id#" value="2"> J-1 à 5h</label>
      </div>
      <div>
        <button id="btnMoins_#id#">−</button>
        <span id="stackWidthDisplay_#id#">#stackWidth#</span> min
        <button id="btnPlus_#id#">+</button>
      </div>
    </div>
  </div>

  <!-- Conteneur pour le graphique Highcharts -->
  <div id="container_#id#" style="width:100%;height:calc(100% - 60px);margin-top:8px;"></div>
</div>

<script>
(() => {
  // --- Initialisation des variables
  const widgetId = "#id#";  // ID unique du widget pour différencier plusieurs instances
  const prefix = "graph_" + widgetId;  // Préfixe utilisé pour stocker les données persistantes dans Jeedom

  // Fonction utilitaire : retourne la valeur si définie sinon la valeur par défaut
  const valeurDefinie = (val, def) => (val === undefined || val === "" || /^#.*#$/.test(val)) ? def : val;

  // --- Paramètres configurables du widget
  const TITLE_ELECTRICITE = valeurDefinie("#title#", "Electricité");  // Titre du graphique
  const TITLE_COLOR       = valeurDefinie("#titleColor#", "#ffffff"); // Couleur du titre et commandes
  const HEADER_BG_COLOR   = valeurDefinie("#headerBgColor#", "rgba(0,0,0,0.4)"); // Fond du bandeau supérieur
  const DEFAULT_DUREE     = parseInt(valeurDefinie("#defaultDuree#", "2"),10); // Durée par défaut
  let stackWidth          = parseInt(valeurDefinie("#stackWidth#", "15"),10); // Largeur du stack en minutes

  // IDs des commandes Jeedom
  const ID_PRODUCTION   = parseInt(valeurDefinie("#idProduction#", "0"),10);
  const ID_CONSOMMATION = parseInt(valeurDefinie("#idConso#", "0"),10);
  const ID_EXPORTATION  = parseInt(valeurDefinie("#idExport#", "0"),10);
  const ID_BATTERIE     = parseInt(valeurDefinie("#idBatterie#", "0"),10);

  // Noms des séries affichées dans le graphique
  const NAME_PRODUCTION   = valeurDefinie("#nameProduction#", "Production solaire");
  const NAME_CONSOMMATION = valeurDefinie("#nameConso#", "Consommation globale");
  const NAME_EXPORTATION  = valeurDefinie("#nameExport#", "Utilisation réseau");
  const NAME_BATTERIE     = valeurDefinie("#nameBatterie#", "Utilisation batterie");

  // Couleurs des séries
  const COLOR_PRODUCTION   = valeurDefinie("#colorProduction#", "#01b4de");
  const COLOR_CONSOMMATION = valeurDefinie("#colorConso#", "#f37320");
  const COLOR_EXPORTATION  = valeurDefinie("#colorExport#", "#6c7073");
  const COLOR_BATTERIE     = valeurDefinie("#colorBatterie#", "#008000");

  const stackSteps = [2,5,10,15,20,30,60]; // Valeurs possibles pour le stackWidth
  let duree = DEFAULT_DUREE;               // Durée courante

  // Fonction utilitaire pour récupérer un élément HTML en ajoutant le widgetId
  const getEl = id => document.getElementById(id + "_" + widgetId);

  // --- Fonctions pour interagir avec Jeedom
  // Récupérer la valeur d'une variable
  const getData = (key, callback) => {
    fetch(`../plugins/script/data/variableGetSet.php?action=get&key=${encodeURIComponent(key)}`)
      .then(r=>r.json())
      .then(data=>callback(data.success?data.value:null))
      .catch(()=>callback(null));
  };
  // Sauvegarder une valeur dans Jeedom
  const setData = (key,value)=>{
    fetch(`../plugins/script/data/variableGetSet.php?action=set&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`)
      .catch(console.error);
  };
  // Formater un timestamp au format Jeedom
  const formatDateJeedom = ts=>{
    const d=new Date(ts), pad=n=>String(n).padStart(2,"0");
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  };

  // --- Fonction pour modifier la largeur du stack
  function changerStack(direction){
    const index=stackSteps.indexOf(stackWidth);
    const newIndex=Math.max(0,Math.min(stackSteps.length-1,index+direction));
    if(newIndex!==index){
      stackWidth=stackSteps[newIndex]; // Mise à jour du stackWidth
      getEl("stackWidthDisplay").textContent=stackWidth; // Affichage sur le widget
      setData(prefix+"_stackWidth",stackWidth); // Sauvegarde
      chargerDonnees(); // Rechargement des données
    }
  }

  // --- Charger les paramètres sauvegardés et initialiser les événements
  function chargerParametresEtDonnées(){
    let nbOK=0;
    // Charger la durée sauvegardée
    getData(prefix+"_duree",val=>{
      duree=val!==null?parseInt(val):DEFAULT_DUREE;
      document.querySelector(`input[name="duree_${widgetId}"][value="${duree}"]`).checked=true;
      if(++nbOK===2) chargerDonnees();
    });
    // Charger la largeur de stack sauvegardée
    getData(prefix+"_stackWidth",val=>{
      stackWidth=val!==null?parseInt(val):stackWidth;
      getEl("stackWidthDisplay").textContent=stackWidth;
      if(++nbOK===2) chargerDonnees();
    });
    // Gestion du changement de durée via les boutons radio
    document.querySelectorAll(`input[name="duree_${widgetId}"]`).forEach(radio=>{
      radio.addEventListener("change",function(){
        duree=parseInt(this.value);
        setData(prefix+"_duree",duree); // Sauvegarde
        chargerDonnees(); // Rechargement des données
      });
    });
    // Gestion des boutons + / - pour stackWidth
    getEl("btnMoins").addEventListener("click",()=>changerStack(-1));
    getEl("btnPlus").addEventListener("click",()=>changerStack(1));
  }

  // --- Charger les données depuis Jeedom et préparer les séries pour Highcharts
  function chargerDonnees(){
    const now=new Date();
    const endTime=now.getTime();
    // Calcul du début selon la durée sélectionnée
    const startTime=(duree===1)? now.getTime()-24*60*60*1000 : new Date(now.getFullYear(),now.getMonth(),now.getDate()-1,5,0,0,0).getTime();
    const puissances={prod:new Map(),cons:new Map(),exp:new Map(),imp:new Map()};
    const arrondiXmin=ts=>Math.floor(ts/(stackWidth*60*1000))*(stackWidth*60*1000); // Arrondi des timestamps
    let chargements=0;

    // Définition des commandes et leurs facteurs (+ ou -)
    const commandes={
      [ID_PRODUCTION]:{map:puissances.prod,facteur:1},
      [ID_CONSOMMATION]:{map:puissances.cons,facteur:-1},
      [ID_EXPORTATION]:{map:puissances.exp,facteur:-1},
      [ID_BATTERIE]:{map:puissances.imp,facteur:1}
    };

    // Récupération des historiques pour chaque commande
    Object.entries(commandes).forEach(([cmd_id,obj])=>{
      jeedom.history.get({
        cmd_id:parseInt(cmd_id),
        dateStart:formatDateJeedom(startTime),
        dateEnd:formatDateJeedom(endTime),
        success:result=>{
          result.data.forEach(entry=>{
            const t=arrondiXmin(entry[0]+new Date().getTimezoneOffset()*60000);
            const val=obj.facteur*entry[1];
            const bucket=obj.map.get(t)||{sum:0,count:0};
            bucket.sum+=val; bucket.count++;
            obj.map.set(t,bucket);
          });
          if(++chargements===4) dessinerGraphique(startTime,endTime,puissances);
        }
      });
    });
  }

  // --- Dessiner le graphique Highcharts avec les données
  function dessinerGraphique(startTime,endTime,puissances){
    const allTimestamps=Array.from(new Set([
      ...puissances.prod.keys(),...puissances.cons.keys(),
      ...puissances.exp.keys(),...puissances.imp.keys()
    ])).sort((a,b)=>a-b);

    const getAvg=b=>(b&&b.count)?b.sum/b.count:0;

    const serie_prod=[],serie_imp=[],serie_cons=[],serie_exp=[];
    allTimestamps.forEach(ts=>{
      const avgProd=getAvg(puissances.prod.get(ts));
      const avgImp=getAvg(puissances.imp.get(ts));
      const avgCons=getAvg(puissances.cons.get(ts));
      const avgExp=getAvg(puissances.exp.get(ts));
      const net=avgProd+avgImp+avgCons+avgExp;
      let correctedExp=avgExp; if(Math.abs(net)>0.5) correctedExp-=net;
      serie_prod.push([ts,Math.round(avgProd)]);
      serie_imp.push([ts,Math.round(avgImp)]);
      serie_cons.push([ts,Math.round(avgCons)]);
      serie_exp.push([ts,Math.round(correctedExp)]);
    });

    Highcharts.setOptions({global:{useUTC:false}});

    // Création du graphique
    const chart=Highcharts.chart("container_"+widgetId,{
      chart:{type:"column",height:null}, // graphique en colonnes
      title:{text:""}, // pas de titre intégré Highcharts
      xAxis:{type:"datetime",min:startTime,max:endTime,tickInterval:3600000,labels:{format:"{value:%H}h"}}, // Axe X en heure
      yAxis:{visible:false,min:-2600,max:2600,startOnTick:false,endOnTick:false,title:{text:"Puissance (W)"}}, // Axe Y masqué
      tooltip:{xDateFormat:"%H:%M",shared:true,formatter:function(){
        const pointStart=this.x,pointEnd=pointStart+stackWidth*60000;
        const f=ts=>Highcharts.dateFormat("%H:%M",ts);
        let s=`<b>${f(pointStart)} → ${f(pointEnd)}</b><br/>`;
        this.points.forEach(p=>{
          const val=Math.abs(p.y);
          if(val!==0) s+=`<span style="color:${p.series.color}">●</span> ${p.series.name}: <b>${Math.trunc(val)}</b><br/>`;
        });
        return s;
      }},
      plotOptions:{column:{stacking:"normal",groupPadding:0.1,pointPadding:0,borderWidth:0}}, // Colonnes empilées
      series:[
        {name:NAME_EXPORTATION,data:serie_exp,stack:"Puissance",color:COLOR_EXPORTATION},
        {name:NAME_BATTERIE,data:serie_imp,stack:"Puissance",color:COLOR_BATTERIE},
        {name:NAME_PRODUCTION,data:serie_prod,stack:"Puissance",color:COLOR_PRODUCTION},
        {name:NAME_CONSOMMATION,data:serie_cons,stack:"Puissance",color:COLOR_CONSOMMATION}
      ]
    });
    document.getElementById("container_"+widgetId).chart=chart;
    ajusterHauteur(); // Ajuste la taille du graphique
  }

  // --- Ajuster la hauteur du graphique en fonction de la taille du widget
  function ajusterHauteur(){
    const container=getEl("container");
    const header=getEl("graphHeader");
    if(!container||!container.chart) return;
    const parent=getEl("graphContainer");
    const availableHeight=parent.clientHeight-header.offsetHeight-10;
    container.chart.setSize(null,Math.max(200,availableHeight),false);
  }

  // Recalcul de la taille lors du redimensionnement de la fenêtre
  window.addEventListener("resize",ajusterHauteur);

  // --- Appliquer les couleurs configurées au titre et au bandeau
  const titleEl=getEl("graphTitle");
  const headerEl=getEl("graphHeader");
  titleEl.textContent=TITLE_ELECTRICITE; // Titre
  titleEl.style.color=TITLE_COLOR;       // Couleur du titre
  headerEl.style.backgroundColor=HEADER_BG_COLOR; // Fond du bandeau
  headerEl.style.color=TITLE_COLOR;      // Couleur des textes de commandes

  // --- Initialisation : chargement des paramètres et données
  chargerParametresEtDonnées();
})();
</script>

Une version avec un paramétre supplémentaire, le type de graph.
Tous les types ne donnent pas de résultats, à vous de réaliser vos tests pour choisir ce qui vous convient.
Exemple :

<template>
  <!-- Définition des paramètres configurables du widget avec leurs valeurs par défaut -->
  <div>title : Titre du graphique. [ défaut : "Electricité" ]</div>
  <div>defaultDuree : Durée par défaut. 1 = 24h, 2 = J-1 à 5h. [ défaut : 2 ]</div>
  <div>stackWidth : Largeur du stack en minutes. [ défaut : 15 ]</div>
 <div>graphType : Type de graphique Highcharts. Options possibles : "line", "spline", "area", "areaspline", "column", "bar", "pie", "scatter", "bubble", "columnrange", "arearange", "areasplinerange", "gauge", "boxplot", "waterfall", "polygon". [ défaut : "column" ]</div>
  <div>idProduction : ID de la commande Production. [ obligatoire ]</div>
  <div>idConso : ID de la commande Consommation. [ obligatoire ]</div>
  <div>idExport : ID de la commande Exportation. [ obligatoire ]</div>
  <div>idBatterie : ID de la commande Batterie. [ obligatoire ]</div>
  <div>nameProduction : Nom de la courbe Production. [ défaut : "Production solaire" ]</div>
  <div>nameConso : Nom de la courbe Consommation. [ défaut : "Consommation globale" ]</div>
  <div>nameExport : Nom de la courbe Exportation. [ défaut : "Utilisation réseau" ]</div>
  <div>nameBatterie : Nom de la courbe Batterie. [ défaut : "Utilisation batterie" ]</div>
  <div>colorProduction : Couleur de la courbe Production. [ défaut : "#01b4de" ]</div>
  <div>colorConso : Couleur de la courbe Consommation. [ défaut : "#f37320" ]</div>
  <div>colorExport : Couleur de la courbe Exportation. [ défaut : "#6c7073" ]</div>
  <div>colorBatterie : Couleur de la courbe Batterie. [ défaut : "#008000" ]</div>
  <div>titleColor : Couleur du titre et des commandes. [ défaut : "#ffffff" ]</div>
  <div>headerBgColor : Couleur de fond du bandeau supérieur. [ défaut : "rgba(0,0,0,0.4)" ]</div>
</template>

<style>
/* Style des boutons radio */
input[type="radio"] {
  accent-color: #01b4de; /* couleur par défaut du thème */
  transform: scale(1.2);
  margin-right: 5px;
}

/* Style des boutons + et - */
button[id^="btn"] {
	background-color: #01b4de;
  color: white;
  border: none;
  border-radius: 50%!important; /* rend le bouton rond */
  width: 28px;
  height: 28px;
  cursor: pointer;
  font-size: 16px;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease;
}

button[id^="btn"]:hover {
  background-color: #028bb0;
}

button[id^="btn"]:active {
  background-color: #016f8b;
}
</style>

<div id="graphContainer_#id#" style="width:100%;height:100%;min-height:300px;position:relative;">
  <!-- Bandeau supérieur du widget : contient le titre et les commandes -->
  <div id="graphHeader_#id#" style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;box-sizing:border-box;border-radius:6px;flex-wrap:wrap;">
    <!-- Titre du graphique -->
    <div id="graphTitle_#id#" style="text-align:left;font-size:18px;margin-left:10px;">#title#</div>
    <!-- Section des commandes (radio pour durée et boutons pour stackWidth) -->
    <div style="display:flex;align-items:center;gap:15px;">
      <div>
        <label><input type="radio" name="duree_#id#" value="1"> 24h</label>
        <label style="margin-left:10px;"><input type="radio" name="duree_#id#" value="2"> J-1 à 5h</label>
      </div>
      <div>
        <button id="btnMoins_#id#">−</button>
        <span id="stackWidthDisplay_#id#">#stackWidth#</span> min
        <button id="btnPlus_#id#">+</button>
      </div>
    </div>
  </div>

  <!-- Conteneur pour le graphique Highcharts -->
  <div id="container_#id#" style="width:100%;height:calc(100% - 60px);margin-top:8px;"></div>
</div>

<script>
(() => {
  // --- Initialisation des variables
  const widgetId = "#id#";  // ID unique du widget pour différencier plusieurs instances
  const prefix = "graph_" + widgetId;  // Préfixe utilisé pour stocker les données persistantes dans Jeedom

  // Fonction utilitaire : retourne la valeur si définie sinon la valeur par défaut
  const valeurDefinie = (val, def) => (val === undefined || val === "" || /^#.*#$/.test(val)) ? def : val;

  // --- Paramètres configurables du widget
  const TITLE_ELECTRICITE = valeurDefinie("#title#", "Electricité");  // Titre du graphique
  const TITLE_COLOR       = valeurDefinie("#titleColor#", "#ffffff"); // Couleur du titre et commandes
  const HEADER_BG_COLOR   = valeurDefinie("#headerBgColor#", "rgba(0,0,0,0.4)"); // Fond du bandeau supérieur
  const DEFAULT_DUREE     = parseInt(valeurDefinie("#defaultDuree#", "2"),10); // Durée par défaut
  let stackWidth          = parseInt(valeurDefinie("#stackWidth#", "15"),10); // Largeur du stack en minutes
  const GRAPH_TYPE        = valeurDefinie("#graphType#", "column"); // Type de graphique Highcharts
  // IDs des commandes Jeedom
  const ID_PRODUCTION   = parseInt(valeurDefinie("#idProduction#", "0"),10);
  const ID_CONSOMMATION = parseInt(valeurDefinie("#idConso#", "0"),10);
  const ID_EXPORTATION  = parseInt(valeurDefinie("#idExport#", "0"),10);
  const ID_BATTERIE     = parseInt(valeurDefinie("#idBatterie#", "0"),10);

  // Noms des séries affichées dans le graphique
  const NAME_PRODUCTION   = valeurDefinie("#nameProduction#", "Production solaire");
  const NAME_CONSOMMATION = valeurDefinie("#nameConso#", "Consommation globale");
  const NAME_EXPORTATION  = valeurDefinie("#nameExport#", "Utilisation réseau");
  const NAME_BATTERIE     = valeurDefinie("#nameBatterie#", "Utilisation batterie");

  // Couleurs des séries
  const COLOR_PRODUCTION   = valeurDefinie("#colorProduction#", "#01b4de");
  const COLOR_CONSOMMATION = valeurDefinie("#colorConso#", "#f37320");
  const COLOR_EXPORTATION  = valeurDefinie("#colorExport#", "#6c7073");
  const COLOR_BATTERIE     = valeurDefinie("#colorBatterie#", "#008000");

  const stackSteps = [2,5,10,15,20,30,60]; // Valeurs possibles pour le stackWidth
  let duree = DEFAULT_DUREE;               // Durée courante

  // Fonction utilitaire pour récupérer un élément HTML en ajoutant le widgetId
  const getEl = id => document.getElementById(id + "_" + widgetId);

  // --- Fonctions pour interagir avec Jeedom
  // Récupérer la valeur d'une variable
  const getData = (key, callback) => {
    fetch(`../plugins/script/data/variableGetSet.php?action=get&key=${encodeURIComponent(key)}`)
      .then(r=>r.json())
      .then(data=>callback(data.success?data.value:null))
      .catch(()=>callback(null));
  };
  // Sauvegarder une valeur dans Jeedom
  const setData = (key,value)=>{
    fetch(`../plugins/script/data/variableGetSet.php?action=set&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`)
      .catch(console.error);
  };
  // Formater un timestamp au format Jeedom
  const formatDateJeedom = ts=>{
    const d=new Date(ts), pad=n=>String(n).padStart(2,"0");
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  };

  // --- Fonction pour modifier la largeur du stack
  function changerStack(direction){
    const index=stackSteps.indexOf(stackWidth);
    const newIndex=Math.max(0,Math.min(stackSteps.length-1,index+direction));
    if(newIndex!==index){
      stackWidth=stackSteps[newIndex]; // Mise à jour du stackWidth
      getEl("stackWidthDisplay").textContent=stackWidth; // Affichage sur le widget
      setData(prefix+"_stackWidth",stackWidth); // Sauvegarde
      chargerDonnees(); // Rechargement des données
    }
  }

  // --- Charger les paramètres sauvegardés et initialiser les événements
  function chargerParametresEtDonnées(){
    let nbOK=0;
    // Charger la durée sauvegardée
    getData(prefix+"_duree",val=>{
      duree=val!==null?parseInt(val):DEFAULT_DUREE;
      document.querySelector(`input[name="duree_${widgetId}"][value="${duree}"]`).checked=true;
      if(++nbOK===2) chargerDonnees();
    });
    // Charger la largeur de stack sauvegardée
    getData(prefix+"_stackWidth",val=>{
      stackWidth=val!==null?parseInt(val):stackWidth;
      getEl("stackWidthDisplay").textContent=stackWidth;
      if(++nbOK===2) chargerDonnees();
    });
    // Gestion du changement de durée via les boutons radio
    document.querySelectorAll(`input[name="duree_${widgetId}"]`).forEach(radio=>{
      radio.addEventListener("change",function(){
        duree=parseInt(this.value);
        setData(prefix+"_duree",duree); // Sauvegarde
        chargerDonnees(); // Rechargement des données
      });
    });
    // Gestion des boutons + / - pour stackWidth
    getEl("btnMoins").addEventListener("click",()=>changerStack(-1));
    getEl("btnPlus").addEventListener("click",()=>changerStack(1));
  }

  // --- Charger les données depuis Jeedom et préparer les séries pour Highcharts
  function chargerDonnees(){
    const now=new Date();
    const endTime=now.getTime();
    // Calcul du début selon la durée sélectionnée
    const startTime=(duree===1)? now.getTime()-24*60*60*1000 : new Date(now.getFullYear(),now.getMonth(),now.getDate()-1,5,0,0,0).getTime();
    const puissances={prod:new Map(),cons:new Map(),exp:new Map(),imp:new Map()};
    const arrondiXmin=ts=>Math.floor(ts/(stackWidth*60*1000))*(stackWidth*60*1000); // Arrondi des timestamps
    let chargements=0;

    // Définition des commandes et leurs facteurs (+ ou -)
    const commandes={
      [ID_PRODUCTION]:{map:puissances.prod,facteur:1},
      [ID_CONSOMMATION]:{map:puissances.cons,facteur:-1},
      [ID_EXPORTATION]:{map:puissances.exp,facteur:-1},
      [ID_BATTERIE]:{map:puissances.imp,facteur:1}
    };

    // Récupération des historiques pour chaque commande
    Object.entries(commandes).forEach(([cmd_id,obj])=>{
      jeedom.history.get({
        cmd_id:parseInt(cmd_id),
        dateStart:formatDateJeedom(startTime),
        dateEnd:formatDateJeedom(endTime),
        success:result=>{
          result.data.forEach(entry=>{
            const t=arrondiXmin(entry[0]+new Date().getTimezoneOffset()*60000);
            const val=obj.facteur*entry[1];
            const bucket=obj.map.get(t)||{sum:0,count:0};
            bucket.sum+=val; bucket.count++;
            obj.map.set(t,bucket);
          });
          if(++chargements===4) dessinerGraphique(startTime,endTime,puissances);
        }
      });
    });
  }

  // --- Dessiner le graphique Highcharts avec les données
  function dessinerGraphique(startTime,endTime,puissances){
    const allTimestamps=Array.from(new Set([
      ...puissances.prod.keys(),...puissances.cons.keys(),
      ...puissances.exp.keys(),...puissances.imp.keys()
    ])).sort((a,b)=>a-b);

    const getAvg=b=>(b&&b.count)?b.sum/b.count:0;

    const serie_prod=[],serie_imp=[],serie_cons=[],serie_exp=[];
    allTimestamps.forEach(ts=>{
      const avgProd=getAvg(puissances.prod.get(ts));
      const avgImp=getAvg(puissances.imp.get(ts));
      const avgCons=getAvg(puissances.cons.get(ts));
      const avgExp=getAvg(puissances.exp.get(ts));
      const net=avgProd+avgImp+avgCons+avgExp;
      let correctedExp=avgExp; if(Math.abs(net)>0.5) correctedExp-=net;
      serie_prod.push([ts,Math.round(avgProd)]);
      serie_imp.push([ts,Math.round(avgImp)]);
      serie_cons.push([ts,Math.round(avgCons)]);
      serie_exp.push([ts,Math.round(correctedExp)]);
    });

    Highcharts.setOptions({global:{useUTC:false}});

    // Création du graphique
    const chart=Highcharts.chart("container_"+widgetId,{
      chart:{type:GRAPH_TYPE,height:null}, // graphique en colonnes
      title:{text:""}, // pas de titre intégré Highcharts
      xAxis:{type:"datetime",min:startTime,max:endTime,tickInterval:3600000,labels:{format:"{value:%H}h"}}, // Axe X en heure
      yAxis:{visible:false,min:-2600,max:2600,startOnTick:false,endOnTick:false,title:{text:"Puissance (W)"}}, // Axe Y masqué
      tooltip:{xDateFormat:"%H:%M",shared:true,formatter:function(){
        const pointStart=this.x,pointEnd=pointStart+stackWidth*60000;
        const f=ts=>Highcharts.dateFormat("%H:%M",ts);
        let s=`<b>${f(pointStart)} → ${f(pointEnd)}</b><br/>`;
        this.points.forEach(p=>{
          const val=Math.abs(p.y);
          if(val!==0) s+=`<span style="color:${p.series.color}">●</span> ${p.series.name}: <b>${Math.trunc(val)}</b><br/>`;
        });
        return s;
      }},
      plotOptions:{column:{stacking:"normal",groupPadding:0.1,pointPadding:0,borderWidth:0}}, // Colonnes empilées
      series:[
        {name:NAME_EXPORTATION,data:serie_exp,stack:"Puissance",color:COLOR_EXPORTATION},
        {name:NAME_BATTERIE,data:serie_imp,stack:"Puissance",color:COLOR_BATTERIE},
        {name:NAME_PRODUCTION,data:serie_prod,stack:"Puissance",color:COLOR_PRODUCTION},
        {name:NAME_CONSOMMATION,data:serie_cons,stack:"Puissance",color:COLOR_CONSOMMATION}
      ]
    });
    document.getElementById("container_"+widgetId).chart=chart;
    ajusterHauteur(); // Ajuste la taille du graphique
  }

  // --- Ajuster la hauteur du graphique en fonction de la taille du widget
  function ajusterHauteur(){
    const container=getEl("container");
    const header=getEl("graphHeader");
    if(!container||!container.chart) return;
    const parent=getEl("graphContainer");
    const availableHeight=parent.clientHeight-header.offsetHeight-10;
    container.chart.setSize(null,Math.max(200,availableHeight),false);
  }

  // Recalcul de la taille lors du redimensionnement de la fenêtre
  window.addEventListener("resize",ajusterHauteur);

  // --- Appliquer les couleurs configurées au titre et au bandeau
  const titleEl=getEl("graphTitle");
  const headerEl=getEl("graphHeader");
  titleEl.textContent=TITLE_ELECTRICITE; // Titre
  titleEl.style.color=TITLE_COLOR;       // Couleur du titre
  headerEl.style.backgroundColor=HEADER_BG_COLOR; // Fond du bandeau
  headerEl.style.color=TITLE_COLOR;      // Couleur des textes de commandes

  // --- Initialisation : chargement des paramètres et données
  chargerParametresEtDonnées();
})();
</script>

Voici un exemple d’usage

Le code de la dernière version utilisé avec un équipement au survol.

<template>
  <!-- Définition des paramètres configurables du widget avec leurs valeurs par défaut -->
  <div>title : Titre du graphique. [ défaut : "Electricité" ]</div>
  <div>defaultDuree : Durée par défaut. 1 = 24h, 2 = J-1 à 5h. [ défaut : 2 ]</div>
  <div>stackWidth : Largeur du stack en minutes. [ défaut : 15 ]</div>
  <div>graphType : Type de graphique Highcharts. Options possibles : "line", "spline", "area", "areaspline", "column", "bar", "pie", "scatter", "bubble", "columnrange", "arearange", "areasplinerange", "gauge", "boxplot", "waterfall", "polygon". [ défaut : "column" ]</div>
  <div>idProduction : ID de la commande Production. [ obligatoire ]</div>
  <div>idConso : ID de la commande Consommation. [ obligatoire ]</div>
  <div>idExport : ID de la commande Exportation. [ obligatoire ]</div>
  <div>idBatterie : ID de la commande Batterie. [ obligatoire ]</div>
  <div>nameProduction : Nom de la courbe Production. [ défaut : "Production solaire" ]</div>
  <div>nameConso : Nom de la courbe Consommation. [ défaut : "Consommation globale" ]</div>
  <div>nameExport : Nom de la courbe Exportation. [ défaut : "Utilisation réseau" ]</div>
  <div>nameBatterie : Nom de la courbe Batterie. [ défaut : "Utilisation batterie" ]</div>
  <div>colorProduction : Couleur de la courbe Production. [ défaut : "#01b4de" ]</div>
  <div>colorConso : Couleur de la courbe Consommation. [ défaut : "#f37320" ]</div>
  <div>colorExport : Couleur de la courbe Exportation. [ défaut : "#6c7073" ]</div>
  <div>colorBatterie : Couleur de la courbe Batterie. [ défaut : "#008000" ]</div>
  <div>titleColor : Couleur du titre et des commandes. [ défaut : "#ffffff" ]</div>
  <div>headerBgColor : Couleur de fond du bandeau supérieur. [ défaut : "rgba(0,0,0,0.4)" ]</div>
</template>

<style>
#graphHeader_#id# {
  display: flex;
  flex-direction: column; /* Titre au-dessus des commandes */
  align-items: flex-start; /* Alignement à gauche */
  padding: 8px 12px;
  box-sizing: border-box;
  border-radius: 6px;
}

#graphTitle_#id# {
  text-align: left; /* Titre aligné à gauche */
  margin-bottom: 8px; /* Espace entre titre et commandes */
  width: 100%;
}

#graphControls_#id# {
  display: flex;
  justify-content: flex-start; /* commandes alignées à gauche */
  gap: 15px;
  width: 100%;
  flex-wrap: wrap;
}

input[type="radio"] {
  accent-color: #01b4de;
  transform: scale(1.2);
  margin-right: 5px;
}

/* Radio actif devient vert */
input[type="radio"]:checked::before {
  content: '';
  position: absolute;
  top: 0px;
  left: 0px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background-color: #00ff00;
}
input[type="radio"]:focus {
  outline: none;
}
  
button[id^="btn"] {
  background-color: #01b4de;
  color: white;
  border: none;
  border-radius:50%!important;
  width: 28px;
  height: 28px;
  cursor: pointer;
  font-size: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease;
}

button[id^="btn"]:hover {
  background-color: #028bb0;
}

button[id^="btn"]:active {
  background-color: #016f8b;
}

select#graphTypeSelect_#id# {
  margin-left: 10px;
  padding: 4px 6px;
  border-radius: 8px!important;
  border: 1px solid #ccc;
  width: auto;
  max-width: 140px;
  background-color: #01b4de!important;
  color: TITLE_COLOR;
}

select#graphTypeSelect_#id#:hover {
  background-color: #e0f0ff;
}
  
</style>

<div id="graphContainer_#id#" style="width:100%;height:100%;min-height:300px;position:relative;">
  <div id="graphHeader_#id#" style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;box-sizing:border-box;border-radius:6px;flex-wrap:wrap;">
    <div id="graphTitle_#id#" style="text-align:left;font-size:18px;margin-left:10px;">#title#</div>
    <div style="display:flex;align-items:center;gap:15px;">
      <div>
        <label><input type="radio" name="duree_#id#" value="1"> 24h</label>
        <label style="margin-left:10px;"><input type="radio" name="duree_#id#" value="2"> J-1 à 5h</label>
        <select id="graphTypeSelect_#id#">
          <option value="line">line</option>
          <option value="spline">spline</option>
          <option value="area">area</option>
          <option value="areaspline">areaspline</option>
          <option value="column">column</option>
          <option value="bar">bar</option>
          <option value="pie">pie</option>
          <option value="scatter">scatter</option>
          <option value="bubble">bubble</option>
          <option value="columnrange">columnrange</option>
          <option value="arearange">arearange</option>
          <option value="areasplinerange">areasplinerange</option>
          <option value="gauge">gauge</option>
          <option value="boxplot">boxplot</option>
          <option value="waterfall">waterfall</option>
          <option value="polygon">polygon</option>
        </select>
      </div>
      <div>
        <button id="btnMoins_#id#">−</button>
        <span id="stackWidthDisplay_#id#">#stackWidth#</span> min
        <button id="btnPlus_#id#">+</button>
      </div>
    </div>
  </div>
  <div id="container_#id#" style="width:100%;height:calc(100% - 60px);margin-top:8px;"></div>
</div>

<script>
(() => {
  // --- Initialisation des variables
  const widgetId = "#id#";  // ID unique du widget pour différencier plusieurs instances
  const prefix = "graph_" + widgetId;  // Préfixe utilisé pour stocker les données persistantes dans Jeedom

  // Fonction utilitaire : retourne la valeur si définie sinon la valeur par défaut
  const valeurDefinie = (val, def) => (val === undefined || val === "" || /^#.*#$/.test(val)) ? def : val;

  // --- Paramètres configurables du widget
  const TITLE_ELECTRICITE = valeurDefinie("#title#", "Electricité");  // Titre du graphique
  const TITLE_COLOR       = valeurDefinie("#titleColor#", "#ffffff"); // Couleur du titre et commandes
  const HEADER_BG_COLOR   = valeurDefinie("#headerBgColor#", "rgba(0,0,0,0.4)"); // Fond du bandeau supérieur
  const DEFAULT_DUREE     = parseInt(valeurDefinie("#defaultDuree#", "2"),10); // Durée par défaut
  let stackWidth          = parseInt(valeurDefinie("#stackWidth#", "15"),10); // Largeur du stack en minutes
  let GRAPH_TYPE          = valeurDefinie("#graphType#", "column"); // Type de graphique Highcharts

  // IDs des commandes Jeedom
  const ID_PRODUCTION   = parseInt(valeurDefinie("#idProduction#", "0"),10);
  const ID_CONSOMMATION = parseInt(valeurDefinie("#idConso#", "0"),10);
  const ID_EXPORTATION  = parseInt(valeurDefinie("#idExport#", "0"),10);
  const ID_BATTERIE     = parseInt(valeurDefinie("#idBatterie#", "0"),10);

  // Noms des séries affichées dans le graphique
  const NAME_PRODUCTION   = valeurDefinie("#nameProduction#", "Production solaire");
  const NAME_CONSOMMATION = valeurDefinie("#nameConso#", "Consommation globale");
  const NAME_EXPORTATION  = valeurDefinie("#nameExport#", "Utilisation réseau");
  const NAME_BATTERIE     = valeurDefinie("#nameBatterie#", "Utilisation batterie");

  // Couleurs des séries
  const COLOR_PRODUCTION   = valeurDefinie("#colorProduction#", "#01b4de");
  const COLOR_CONSOMMATION = valeurDefinie("#colorConso#", "#f37320");
  const COLOR_EXPORTATION  = valeurDefinie("#colorExport#", "#6c7073");
  const COLOR_BATTERIE     = valeurDefinie("#colorBatterie#", "#008000");

  const stackSteps = [2,5,10,15,20,30,60]; // Valeurs possibles pour le stackWidth
  let duree = DEFAULT_DUREE;               // Durée courante

  // Fonction utilitaire pour récupérer un élément HTML en ajoutant le widgetId
  const getEl = id => document.getElementById(id + "_" + widgetId);

  // --- Fonctions pour interagir avec Jeedom
  // Récupérer la valeur d'une variable
  const getData = (key, callback) => {
    fetch(`../plugins/script/data/variableGetSet.php?action=get&key=${encodeURIComponent(key)}`)
      .then(r=>r.json())
      .then(data=>callback(data.success?data.value:null))
      .catch(()=>callback(null));
  };
  // Sauvegarder une valeur dans Jeedom
  const setData = (key,value)=>{
    fetch(`../plugins/script/data/variableGetSet.php?action=set&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`)
      .catch(console.error);
  };
  // Formater un timestamp au format Jeedom
  const formatDateJeedom = ts=>{
    const d=new Date(ts), pad=n=>String(n).padStart(2,"0");
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  };

  // --- Fonction pour modifier la largeur du stack
  function changerStack(direction){
    const index=stackSteps.indexOf(stackWidth);
    const newIndex=Math.max(0,Math.min(stackSteps.length-1,index+direction));
    if(newIndex!==index){
      stackWidth=stackSteps[newIndex]; // Mise à jour du stackWidth
      getEl("stackWidthDisplay").textContent=stackWidth; // Affichage sur le widget
      setData(prefix+"_stackWidth",stackWidth); // Sauvegarde
      chargerDonnees(); // Rechargement des données
    }
  }

  // --- Charger les paramètres sauvegardés et initialiser les événements
  function chargerParametresEtDonnées(){
    let nbOK=0;
    // Charger la durée sauvegardée
    getData(prefix+"_duree",val=>{
      duree=val!==null?parseInt(val):DEFAULT_DUREE;
      document.querySelector(`input[name="duree_${widgetId}"][value="${duree}"]`).checked=true;
      if(++nbOK===3) chargerDonnees();
    });
    // Charger la largeur de stack sauvegardée
    getData(prefix+"_stackWidth",val=>{
      stackWidth=val!==null?parseInt(val):stackWidth;
      getEl("stackWidthDisplay").textContent=stackWidth;
      if(++nbOK===3) chargerDonnees();
    });
    // Charger le type de graphique sauvegardé
    getData(prefix+"_graphType",val=>{
      GRAPH_TYPE=val!==null?val:GRAPH_TYPE;
      const select = document.getElementById(`graphTypeSelect_${widgetId}`);
      if(select) select.value = GRAPH_TYPE;
      if(++nbOK===3) chargerDonnees();
    });

    // Gestion du changement de durée via les boutons radio
    document.querySelectorAll(`input[name="duree_${widgetId}"]`).forEach(radio=>{
      radio.addEventListener("change",function(){
        duree=parseInt(this.value);
        setData(prefix+"_duree",duree); // Sauvegarde
        chargerDonnees(); // Rechargement des données
      });
    });
    // Gestion des boutons + / - pour stackWidth
    getEl("btnMoins").addEventListener("click",()=>changerStack(-1));
    getEl("btnPlus").addEventListener("click",()=>changerStack(1));

    // Gestion du select graphType (déjà présent dans le DOM)
    const graphSelect = document.getElementById(`graphTypeSelect_${widgetId}`);
    if(graphSelect){
      graphSelect.addEventListener('change', ()=>{
        GRAPH_TYPE = graphSelect.value;
        setData(prefix+"_graphType", GRAPH_TYPE);
        chargerDonnees();
      });
    }
  }

  // --- Charger les données depuis Jeedom et préparer les séries pour Highcharts
  function chargerDonnees(){
    const now=new Date();
    const endTime=now.getTime();
    const startTime=(duree===1)? now.getTime()-24*60*60*1000 : new Date(now.getFullYear(),now.getMonth(),now.getDate()-1,5,0,0,0).getTime();
    const puissances={prod:new Map(),cons:new Map(),exp:new Map(),imp:new Map()};
    const arrondiXmin=ts=>Math.floor(ts/(stackWidth*60*1000))*(stackWidth*60*1000); // Arrondi des timestamps
    let chargements=0;

    // Définition des commandes et leurs facteurs (+ ou -)
    const commandes={
      [ID_PRODUCTION]:{map:puissances.prod,facteur:1},
      [ID_CONSOMMATION]:{map:puissances.cons,facteur:-1},
      [ID_EXPORTATION]:{map:puissances.exp,facteur:-1},
      [ID_BATTERIE]:{map:puissances.imp,facteur:1}
    };

    // Récupération des historiques pour chaque commande
    Object.entries(commandes).forEach(([cmd_id,obj])=>{
      jeedom.history.get({
        cmd_id:parseInt(cmd_id),
        dateStart:formatDateJeedom(startTime),
        dateEnd:formatDateJeedom(endTime),
        success:result=>{
          result.data.forEach(entry=>{
            const t=arrondiXmin(entry[0]+new Date().getTimezoneOffset()*60000);
            const val=obj.facteur*entry[1];
            const bucket=obj.map.get(t)||{sum:0,count:0};
            bucket.sum+=val; bucket.count++;
            obj.map.set(t,bucket);
          });
          if(++chargements===4) dessinerGraphique(startTime,endTime,puissances);
        }
      });
    });
  }

  // --- Dessiner le graphique Highcharts avec les données
  function dessinerGraphique(startTime,endTime,puissances){
    const allTimestamps=Array.from(new Set([
      ...puissances.prod.keys(),...puissances.cons.keys(),
      ...puissances.exp.keys(),...puissances.imp.keys()
    ])).sort((a,b)=>a-b);

    const getAvg=b=>(b&&b.count)?b.sum/b.count:0;

    const serie_prod=[],serie_imp=[],serie_cons=[],serie_exp=[];
    allTimestamps.forEach(ts=>{
      const avgProd=getAvg(puissances.prod.get(ts));
      const avgImp=getAvg(puissances.imp.get(ts));
      const avgCons=getAvg(puissances.cons.get(ts));
      const avgExp=getAvg(puissances.exp.get(ts));
      const net=avgProd+avgImp+avgCons+avgExp;
      let correctedExp=avgExp; if(Math.abs(net)>0.5) correctedExp-=net;
      serie_prod.push([ts,Math.round(avgProd)]);
      serie_imp.push([ts,Math.round(avgImp)]);
      serie_cons.push([ts,Math.round(avgCons)]);
      serie_exp.push([ts,Math.round(correctedExp)]);
    });

    Highcharts.setOptions({global:{useUTC:false}});

    // Création du graphique en utilisant le type sélectionné
    const chart=Highcharts.chart("container_"+widgetId,{
      chart:{type:GRAPH_TYPE,height:null}, // <-- utilisation de GRAPH_TYPE ici
      title:{text:""}, // pas de titre intégré Highcharts
      xAxis:{type:"datetime",min:startTime,max:endTime,tickInterval:3600000,labels:{format:"{value:%H}h"}}, // Axe X en heure
      yAxis:{visible:false,min:null,max:null,startOnTick:false,endOnTick:false,title:{text:"Puissance (W)"}}, // Axe Y masqué
      tooltip:{xDateFormat:"%H:%M",shared:true,formatter:function(){
        const pointStart=this.x,pointEnd=pointStart+stackWidth*60000;
        const f=ts=>Highcharts.dateFormat("%H:%M",ts);
        let s=`<b>${f(pointStart)} → ${f(pointEnd)}</b><br/>`;
        this.points.forEach(p=>{
          const val=Math.abs(p.y);
          if(val!==0) s+=`<span style="color:${p.series.color}">●</span> ${p.series.name}: <b>${Math.trunc(val)}</b><br/>`;
        });
        return s;
      }},
      plotOptions:{column:{stacking:"normal",groupPadding:0.1,pointPadding:0,borderWidth:0}}, // Colonnes empilées
      series:[
        {name:NAME_EXPORTATION,data:serie_exp,stack:"Puissance",color:COLOR_EXPORTATION},
        {name:NAME_BATTERIE,data:serie_imp,stack:"Puissance",color:COLOR_BATTERIE},
        {name:NAME_PRODUCTION,data:serie_prod,stack:"Puissance",color:COLOR_PRODUCTION},
        {name:NAME_CONSOMMATION,data:serie_cons,stack:"Puissance",color:COLOR_CONSOMMATION}
      ]
    });
    document.getElementById("container_"+widgetId).chart=chart;
    ajusterHauteur(); // Ajuste la taille du graphique
  }

  // --- Ajuster la hauteur du graphique en fonction de la taille du widget
  function ajusterHauteur(){
    const container=getEl("container");
    const header=getEl("graphHeader");
    if(!container||!container.chart) return;
    const parent=getEl("graphContainer");
    const availableHeight=parent.clientHeight-header.offsetHeight-10;
    container.chart.setSize(null,Math.max(200,availableHeight),false);
  }

  // Recalcul de la taille lors du redimensionnement de la fenêtre
  window.addEventListener("resize",ajusterHauteur);

  // --- Appliquer les couleurs configurées au titre et au bandeau
  const titleEl=getEl("graphTitle");
  const headerEl=getEl("graphHeader");
  titleEl.textContent=TITLE_ELECTRICITE; // Titre
  titleEl.style.color=TITLE_COLOR;       // Couleur du titre
  headerEl.style.backgroundColor=HEADER_BG_COLOR; // Fond du bandeau
  headerEl.style.color=TITLE_COLOR;      // Couleur des textes de commandes

  // --- Initialisation : chargement des paramètres et données
  chargerParametresEtDonnées();
})();
</script>

Bravo pour les adaptations avec l’ajout de la conso sur batterie notamment !
Moi j’ai pas mais je comprends le besoin !

1 « J'aime »

Bonjour

C’est grâce à ton boulot de fond, j’ai juste rendu le graphique paramétrable pour qu’il soit accessible à plus de monde. Encore mille mercis.
Je m’aperçois qu’il y a tellement de possibilité avec les graphiques, il est dommage que Jeedom soit limité en standard.
Notamment, j’ai vu qu’il y a une option pour afficher la table de données. Cette option n’est pas utilisée dans Jeedom. Est-ce que tu sais si c’est une question de licence ?

Hello, merci pour les compliments !
Non je ne me suis pas intéressé aux tables de données.

1 « J'aime »

Bonjour

Voici une version (la dernière) qui affiche la table des données et permet d’exporter un csv.
J’ai supprimé également la référence au php pour persister des valeurs afin de faciliter la mise en oeuvre.
Enfin, j’ai demandé à GPT de rajouter des commentaires détaillés afin de facilité la réutilisation et la personnalisation pour ceux qui en auraient envie.

<template>
  <!-- Paramètres configurables exposés dans Jeedom -->
  <div>title : Titre du graphique. [ défaut : "Electricité" ]</div>
  <div>graphType : Type de graphique Highcharts. [ défaut : "column" ]</div>
  <div>idProduction : ID de la commande Production. [ obligatoire ]</div>
  <div>idConso : ID de la commande Consommation. [ obligatoire ]</div>
  <div>idExport : ID de la commande Exportation. [ obligatoire ]</div>
  <div>idBatterie : ID de la commande Batterie. [ obligatoire ]</div>
  <div>nameProduction : Nom de la courbe Production. [ défaut : "Production solaire" ]</div>
  <div>nameConso : Nom de la courbe Consommation. [ défaut : "Consommation globale" ]</div>
  <div>nameExport : Nom de la courbe Exportation. [ défaut : "Utilisation réseau" ]</div>
  <div>nameBatterie : Nom de la courbe Batterie. [ défaut : "Utilisation batterie" ]</div>
  <div>colorProduction : Couleur de la courbe Production. [ défaut : "#01b4de" ]</div>
  <div>colorConso : Couleur de la courbe Consommation. [ défaut : "#f37320" ]</div>
  <div>colorExport : Couleur de la courbe Exportation. [ défaut : "#6c7073" ]</div>
  <div>colorBatterie : Couleur de la courbe Batterie. [ défaut : "#008000" ]</div>
  <div>titleColor : Couleur du titre et des commandes. [ défaut : "#ffffff" ]</div>
  <div>headerBgColor : Couleur de fond du bandeau supérieur. [ défaut : "rgba(0,0,0,0.4)" ]</div>
  <div>tableHeight : Hauteur du tableau des données en pixels. [ défaut : 200 ]</div>
  <div>defaultDuree : Durée par défaut pour le graphique. 1 = 24h, 2 = J-1 à 5h. [défaut : 2]</div>
  <div>stackWidth : Finesse du graphique en minutes 2,5,10,15,20,30 ou 60 [défaut : 15]</div>
</template>

<script>
// --- Charger les modules Highcharts uniquement si pas déjà présents dans la page
const scripts = [
  '../3rdparty/highstock/modules/exporting.js',   // Permet export CSV / PNG / PDF
  '../3rdparty/highstock/modules/export-data.js', // Permet l'export des données
  '../3rdparty/highstock/modules/accessibility.js' // Accessibilité (lecture d'écran)
];
scripts.forEach(src => {
  if (!document.querySelector(`script[src="${src}"]`)) {
    const s = document.createElement('script');
    s.src = src;
    document.head.appendChild(s);
  }
});
</script>

<style>
/* --- Styles généraux du widget --- */
  
/* --- Conteneur principal du header du widget --- */
#graphHeader_#id# {
  display: flex;
  flex-direction: column; /* header sur plusieurs lignes si besoin */
  align-items: flex-start;
  padding: 8px 12px;
  box-sizing: border-box;
  border-radius: 6px;
}
  
/* --- Titre du graphique --- */
#graphTitle_#id# {
  text-align:left;
  margin-bottom:8px;
  width:100%;
}

/* --- Conteneur des contrôles (radio, select, boutons) --- */
#graphControls_#id# {
  display:flex;
  justify-content:flex-start;
  gap:15px;
  width:100%;
  flex-wrap:wrap;
}

/* Styles boutons et radio */
/* Bouton radio pour choisir l'horizon */
  input[type="radio"] {
  accent-color:#01b4de; 
  transform:scale(1.2);
  margin-right:5px;
}
  /* Bouton + et - */
button[id^="btn"] {
  background-color:#01b4de;
  color:white;
  border:none;
  border-radius:50%!important;
  width:28px;
  height:28px;
  cursor:pointer;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  transition:0.2s;
}
button[id^="btn"]:hover { background-color:#028bb0; }
button[id^="btn"]:active { background-color:#016f8b; }

/* Sélecteur du type de graphique */
select#graphTypeSelect_#id# {
  margin-left:10px;
  padding:4px 6px;
  border-radius:8px!important;
  border:1px solid #ccc;
  width:auto;
  max-width:140px;
  background-color:#01b4de!important;
  color:#fff;
}

/* --- Tableau des données --- */
  /* --- Styles pour le tableau des données Highcharts --- */
.highcharts-data-table table {
  display: block; /* scroll vertical si nécessaire */
  overflow-y: auto;
  width: 100%;
  border-collapse: collapse;
  box-sizing: border-box;
  padding-right: 12px;
}
  /* --- Styles pour l'en-tête du tableau --- */
.highcharts-data-table thead tr {
  display: table; width: 100%; table-layout: fixed;
  position: sticky; top: 0;
  background: #1F7AE0; color: white; z-index: 2;
}
  /* --- Styles pour les lignes du corps du tableau --- */
.highcharts-data-table tbody tr {
  display: table; width: 100%; table-layout: fixed;
  transition: background-color 0.18s ease, color 0.18s ease;
}
  /* --- Styles pour les cellules (th et td) --- */
.highcharts-data-table th, .highcharts-data-table td {
  text-align: center; vertical-align: middle; padding: 4px 8px; white-space: wrap;
}
</style>

<!-- Structure principale du widget -->
<!-- --- Conteneur principal du widget graphique --- -->
<div id="graphContainer_#id#" 
     style="width:100%; height:100%; min-height:300px; position:relative;">
  <!-- width/height à 100% pour remplir le conteneur parent, min-height pour garder une hauteur minimale -->
  <!-- position: relative permet de positionner des éléments enfants de manière absolue si nécessaire -->

  <!-- --- Header du graphique (titre + contrôles) --- -->
  <div id="graphHeader_#id#" 
       style="width:100%; display:flex; align-items:center; justify-content:space-between; padding:8px 12px; box-sizing:border-box; border-radius:6px; flex-wrap:wrap;">
    <!-- Flexbox pour organiser le titre et les contrôles horizontalement -->
    <!-- flex-wrap: wrap permet aux éléments de passer à la ligne si l'espace est insuffisant -->
    <!-- padding et border-radius pour le style visuel -->

    <!-- --- Titre du graphique --- -->
    <div id="graphTitle_#id#" style="text-align:left; font-size:18px; margin-left:10px;">
      #title#
    </div>
    <!-- Texte du titre aligné à gauche avec un peu de marge -->

    <!-- --- Conteneurs des contrôles utilisateur --- -->
    <div style="display:flex; align-items:center; gap:15px;">
      <!-- Flexbox horizontal avec un écart entre chaque groupe de contrôle -->

      <div>
        <!-- --- Choix de la durée du graphique --- -->
        <label><input type="radio" name="duree_#id#" value="1"> 24h</label>
        <label style="margin-left:10px;"><input type="radio" name="duree_#id#" value="2"> J-1 à 5h</label>
        <!-- Les boutons radio permettent de sélectionner la plage temporelle du graphique -->

        <!-- --- Sélecteur du type de graphique Highcharts --- -->
        <select id="graphTypeSelect_#id#">
          <option value="line">line</option>
          <option value="spline">spline</option>
          <option value="area">area</option>
          <option value="areaspline">areaspline</option>
          <option value="column">column</option>
          <option value="bar">bar</option>
          <option value="polygon">polygon</option>
        </select>
        <!-- Permet à l'utilisateur de choisir le type de graphique (ligne, barres, zone, etc.) -->
      </div>

      <div>
        <!-- --- Contrôle de la finesse du graphique (stackWidth) --- -->
        <button id="btnMoins_#id#">−</button>   <!-- Diminue stackWidth -->
        <span id="stackWidthDisplay_#id#">#stackWidth#</span> min  <!-- Affiche la valeur actuelle de stackWidth -->
        <button id="btnPlus_#id#">+</button>     <!-- Augmente stackWidth -->
      </div>
    </div>
  </div>

  <!-- --- Conteneur du graphique Highcharts --- -->
  <div id="container_#id#" style="width:100%; height:calc(100% - 60px); margin-top:0px;"></div>
  <!-- width à 100% pour remplir horizontalement, height réduit de 60px pour laisser place au header -->
</div>


<script>
(() => {
  const widgetId = "#id#";

  // --- Helper pour récupérer un élément du DOM avec l'ID complet
  const getEl = id => document.getElementById(id + "_" + widgetId);

  // --- Helper pour retourner une valeur définie ou une valeur par défaut
  const valeurDefinie = (val, def) => (val === undefined || val === "" || /^#.*#$/.test(val)) ? def : val;

  /* --- Paramètres du widget --- */
  const TITLE_ELECTRICITE = valeurDefinie("#title#", "Electricité");  // titre graphique
  const TITLE_COLOR       = valeurDefinie("#titleColor#", "#ffffff"); // couleur titre
  const HEADER_BG_COLOR   = valeurDefinie("#headerBgColor#", "rgba(0,0,0,0.4)"); // fond header
  const DEFAULT_DUREE     = parseInt(valeurDefinie("#defaultDuree#", "2"),10); // durée par défaut
  let stackWidth          = parseInt(valeurDefinie("#stackWidth#", "15"),10); // largeur du bucket en minutes
  let GRAPH_TYPE          = valeurDefinie("#graphType#", "column"); // type de graphique

  // --- IDs commandes Jeedom
  const ID_PRODUCTION   = parseInt(valeurDefinie("#idProduction#", "0"),10);
  const ID_CONSOMMATION = parseInt(valeurDefinie("#idConso#", "0"),10);
  const ID_EXPORTATION  = parseInt(valeurDefinie("#idExport#", "0"),10);
  const ID_BATTERIE     = parseInt(valeurDefinie("#idBatterie#", "0"),10);

  // --- Noms séries
  const NAME_PRODUCTION   = valeurDefinie("#nameProduction#", "Production solaire");
  const NAME_CONSOMMATION = valeurDefinie("#nameConso#", "Consommation globale");
  const NAME_EXPORTATION  = valeurDefinie("#nameExport#", "Utilisation réseau");
  const NAME_BATTERIE     = valeurDefinie("#nameBatterie#", "Utilisation batterie");

  // --- Couleurs séries
  const COLOR_PRODUCTION   = valeurDefinie("#colorProduction#", "#01b4de");
  const COLOR_CONSOMMATION = valeurDefinie("#colorConso#", "#f37320");
  const COLOR_EXPORTATION  = valeurDefinie("#colorExport#", "#6c7073");
  const COLOR_BATTERIE     = valeurDefinie("#colorBatterie#", "#008000");

  const TABLE_HEIGHT = parseInt(valeurDefinie("#tableHeight#", "200"),10); // hauteur tableau

  const stackSteps = [2,5,10,15,20,30,60]; // valeurs possibles de stackWidth
  let duree = DEFAULT_DUREE; // durée active

  /* --- Fonction pour changer la finesse du graphe (stackWidth) --- */
  function changerStack(direction) {
    const index = stackSteps.indexOf(stackWidth);
    const newIndex = Math.max(0, Math.min(stackSteps.length - 1, index + direction));
    if (newIndex !== index) {
      stackWidth = stackSteps[newIndex];
      getEl("stackWidthDisplay").textContent = stackWidth; // mise à jour affichage
      chargerDonnees(); // recharge graphique avec nouvelle finesse
    }
  }

  /* --- Convertit timestamp JS en format compatible Jeedom --- */
  const formatDateJeedom = ts => {
    const d = new Date(ts), pad = n => String(n).padStart(2,"0");
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  };

  /* --- Récupère les données Jeedom et prépare les séries --- */
  function chargerDonnees() {
    const now = new Date();
    const endTime = now.getTime();

    // calcul date de début selon durée choisie
    const startTime = (duree===1)
      ? now.getTime()-24*60*60*1000 // 24h
      : new Date(now.getFullYear(), now.getMonth(), now.getDate()-1, 5,0,0,0).getTime(); // J-1 à 5h

    const puissances = { prod:new Map(), cons:new Map(), exp:new Map(), imp:new Map() };
    const arrondiXmin = ts => Math.floor(ts/(stackWidth*60*1000))*(stackWidth*60*1000); // arrondi timestamps
    let chargements = 0;

    // Map des commandes et facteur (certaines valeurs négatives pour inversion)
    const commandes = {
      [ID_PRODUCTION]:   {map:puissances.prod, facteur:1},
      [ID_CONSOMMATION]: {map:puissances.cons, facteur:-1},
      [ID_EXPORTATION]:  {map:puissances.exp,  facteur:-1},
      [ID_BATTERIE]:     {map:puissances.imp,  facteur:1}
    };

    // --- Récupération des historiques Jeedom
    Object.entries(commandes).forEach(([cmd_id,obj])=>{
      jeedom.history.get({
        cmd_id:parseInt(cmd_id),
        dateStart:formatDateJeedom(startTime),
        dateEnd:formatDateJeedom(endTime),
        success: result=>{
          result.data.forEach(entry => {
  			const t = arrondiXmin(entry[0] + new Date().getTimezoneOffset() * 60000);
  			let val = obj.facteur * entry[1];
			  // Si c'est la batterie (ID_BATTERIE) et que la valeur est négative (charge) → on ignore pour ne pas l'ajouter à la consommation (déjà incluse)
  			if (parseInt(cmd_id) === ID_BATTERIE && val < 0) return;
			const bucket = obj.map.get(t) || { sum: 0, count: 0 };
  			bucket.sum += val;
  			bucket.count++;
  			obj.map.set(t, bucket);
			});
          if(++chargements===4) dessinerGraphique(startTime,endTime,puissances);
        }
      });
    });
  }

 
  // --- Fonction dessin du graphique Highcharts ---
	// Paramètres :
	// startTime / endTime : timestamps début/fin du graphique
	// puissances : objet contenant les données pour prod, conso, export, batterie
  function dessinerGraphique(startTime,endTime,puissances){
   // --- Regroupe tous les timestamps uniques des différentes séries et les trie
    const allTimestamps = Array.from(new Set([
      ...puissances.prod.keys(), 
      ...puissances.cons.keys(),
      ...puissances.exp.keys(), 
      ...puissances.imp.keys()
    ])).sort((a,b)=>a-b);

    // --- Fonction pour calculer la moyenne d’un bucket (si count > 0)
    const getAvg = b => (b && b.count)? b.sum/b.count : 0; // moyenne d’un bucket
    // --- Initialisation des séries pour Highcharts
    const serie_prod=[], serie_imp=[], serie_cons=[], serie_exp=[];
	// --- Construction des séries pour chaque timestamp
    allTimestamps.forEach(ts=>{
   	const avgProd = getAvg(puissances.prod.get(ts));   // moyenne production
    const avgImp  = getAvg(puissances.imp.get(ts));    // moyenne batterie
    const avgCons = getAvg(puissances.cons.get(ts));   // moyenne consommation
    const avgExp  = getAvg(puissances.exp.get(ts));    // moyenne export réseau
    const net = avgProd + avgImp + avgCons + avgExp;	// calcul du net
      let correctedExp = avgExp; if(Math.abs(net)>0.5) correctedExp -= net; // corrige les éventuels écarts

      // Ajout des valeurs dans les séries
      serie_prod.push([ts,Math.round(avgProd)]);
      serie_imp.push([ts,Math.round(avgImp)]);
      serie_cons.push([ts,Math.round(avgCons)]);
      serie_exp.push([ts,Math.round(correctedExp)]);
    });

    // --- Configuration globale Highcharts
    Highcharts.setOptions({global:{useUTC:false}});
    
  	// --- Création du graphique
    const chart = Highcharts.chart("container_"+widgetId,{
      chart:{type:GRAPH_TYPE,height:null}, // type choisi par l'utilisateur
      title:{text:""}, // pas de titre dans le graphique
      // axe horizontal définition de l'affichage
   	xAxis: {
      type: "datetime", 
      min: startTime, 
      max: endTime, 
      tickInterval: 3600000,                    // intervalle d'une heure
      labels: { format:"{value:%H}h" }          // format HHh
    },
        // axe vertical - ne pas afficher
	yAxis: {
      visible: false, 
      startOnTick: false, 
      endOnTick: false, 
      title: { text: "Puissance (W)" }
    },
      // --- Tooltip : affiche les infos détaillées au survol d'un point
      tooltip: {
        xDateFormat: "%H:%M",
        shared: true,
        formatter: function () {
          const pointStart = this.x, pointEnd = pointStart + stackWidth * 60000;
          const f = ts => Highcharts.dateFormat("%H:%M", ts);
          let s = `<b>${f(pointStart)} → ${f(pointEnd)}</b><br/>`;

          // Trier les points pour afficher "Consommation" en premier
          const sortedPoints = [];
          let consoPoint = null;

          this.points.forEach(p => {
            if (p.series.name.toLowerCase().includes("conso")) consoPoint = p;
            else sortedPoints.push(p);
          });

          // --- Affichage consommation d'abord
          if (consoPoint) {
            const val = Math.abs(consoPoint.y);
            if (val !== 0)
              s += `<span style="color:${consoPoint.series.color}">●</span> ${consoPoint.series.name}: <b>${Math.trunc(val)}</b><br/>`;
          }

          // --- Séparateur
          s += `<br/><b>Sources :</b><br/>`;

          // --- Puis les autres (production, batterie, export, etc.)
          sortedPoints.forEach(p => {
            const val = Math.abs(p.y);
            if (val !== 0)
              s += `<span style="color:${p.series.color}">●</span> ${p.series.name}: <b>${Math.trunc(val)}</b><br/>`;
          });

          return s;
        }
      },
       // --- Options spécifiques pour les séries de type "column" (colonnes)
      plotOptions:{column:{stacking:"normal",groupPadding:0.1,pointPadding:0,borderWidth:0}},
      
      // --- Active les options d'export du graphique
	  // Permet à l'utilisateur de télécharger le graphique en PNG, PDF ou CSV
      exporting:{enabled:true},
     
      // --- Chaque objet dans "series" correspond à une série de données du graphique
	  // Les séries sont empilées (stack: "Puissance") pour que la somme des valeurs soit visible
      // Les couleurs et noms sont personnalisables pour une lecture plus intuitive
      series:[
        {name:NAME_EXPORTATION,data:serie_exp,stack:"Puissance",color:COLOR_EXPORTATION},
        {name:NAME_BATTERIE,data:serie_imp,stack:"Puissance",color:COLOR_BATTERIE},
        {name:NAME_PRODUCTION,data:serie_prod,stack:"Puissance",color:COLOR_PRODUCTION},
        {name:NAME_CONSOMMATION,data:serie_cons,stack:"Puissance",color:COLOR_CONSOMMATION}
      ]
    });

  
	// --- Gestion du tableau des données et ajout d'un bouton pour le fermer
	Highcharts.addEvent(chart, 'afterViewData', function () {
  	const tableDiv = this.dataTableDiv; // Récupère le conteneur HTML du tableau de données généré par Highcharts

  	// Vérifie si le tableau existe et si le bouton de fermeture n'a pas encore été ajouté
 	 if (tableDiv && !document.getElementById(`closeTable_${widgetId}`)) {

    // Création du bouton "fermer"
    const closeBtn = document.createElement("div");
    closeBtn.id = `closeTable_${widgetId}`;  // ID unique pour ce widget
    closeBtn.textContent = "×";              // Symbole de fermeture
    closeBtn.title = "Fermer le tableau et revenir au graphique"; // Tooltip au survol

    // Style CSS du bouton
    closeBtn.style.cssText = `
      position: sticky; top: 0; right: 0; float: right;   // Position fixe en haut à droite du tableau
      font-size: 20px; font-weight: bold; color: #fff;   // Style du texte
      background: #01b4de; border-radius: 50%;           // Fond bleu et bouton rond
      width: 24px; height: 24px; line-height: 22px;      // Dimensions et centrage vertical
      text-align: center; cursor: pointer; margin: 4px; z-index: 10; // Centrage, clic et superposition
    `;

    // Ajout de l'événement de clic pour fermer le tableau
    closeBtn.addEventListener("click", () => {
      chart.hideData();   // Masque le tableau
      closeBtn.remove();  // Supprime le bouton de fermeture
    });

    // Insère le bouton avant le tableau dans le DOM
    tableDiv.parentNode.insertBefore(closeBtn, tableDiv);
 	 }
	});

	// --- Stocke l'objet chart dans le conteneur pour un accès ultérieur
	getEl("container").chart = chart;

	// --- Ajuste la hauteur du graphique selon la taille du header
	ajusterHauteur();
  	}

  // --- Ajuste la hauteur du graphique en fonction du header
  function ajusterHauteur(){
    const header = getEl("graphHeader");
    const container = getEl("container");
    const totalHeight = getEl("graphContainer").offsetHeight;
    const headerHeight = header.offsetHeight;
    container.style.height = (totalHeight - headerHeight - 0) + "px"; 
  }

  // --- Initialisation header et contrôles ---
  const header = getEl("graphHeader");
  header.style.backgroundColor = HEADER_BG_COLOR;
  header.style.color = TITLE_COLOR;
  getEl("graphTitle").style.color = TITLE_COLOR;

  // --- Gestion interactions utilisateur
  document.querySelectorAll(`input[name="duree_${widgetId}"]`).forEach(radio=>{
    radio.addEventListener("change", function(){
      duree = parseInt(this.value);
      chargerDonnees();
    });
  });
  getEl("btnMoins").addEventListener("click",()=>changerStack(-1));
  getEl("btnPlus").addEventListener("click",()=>changerStack(1));
  const graphSelect = getEl("graphTypeSelect");
  if(graphSelect){
    graphSelect.addEventListener("change",()=>{ GRAPH_TYPE = graphSelect.value; chargerDonnees(); });
  }

  // --- Valeurs initiales
  document.querySelector(`input[name="duree_${widgetId}"][value="${duree}"]`).checked = true;
  getEl("stackWidthDisplay").textContent = stackWidth;
  if (graphSelect) graphSelect.value = GRAPH_TYPE;
  chargerDonnees();

  // --- Fixe la hauteur max du tableau de données
  (function injectTableHeightStyle(){
    const id = "tableHeightStyle_" + widgetId;
    if(document.getElementById(id)) return;
    const s = document.createElement("style");
    s.id = id;
    s.textContent = `
      #graphContainer_${widgetId} .highcharts-data-table table {
        max-height: ${TABLE_HEIGHT}px;
      }
    `;
    document.head.appendChild(s);
  })();

  // --- Ajuste dynamiquement la hauteur du graphique lorsque la fenêtre est redimensionnée
	window.addEventListener("resize", ajusterHauteur);

})();
</script>

Bonsoir,

Voici la version actuelle avec la possibilité de naviguer dans les périodes passées par tranche de 24h.

<template>
  <!-- Paramètres configurables exposés dans Jeedom -->
  <div>title : Titre du graphique. [ défaut : "Electricité" ]</div>
  <div>graphType : Type de graphique Highcharts. [ défaut : "column" ]</div>
  <div>idProduction : ID de la commande Production. [ obligatoire ]</div>
  <div>idConso : ID de la commande Consommation. [ obligatoire ]</div>
  <div>idExport : ID de la commande Exportation. [ obligatoire ]</div>
  <div>idBatterie : ID de la commande Batterie. [ obligatoire ]</div>
  <div>nameProduction : Nom de la courbe Production. [ défaut : "Production solaire" ]</div>
  <div>nameConso : Nom de la courbe Consommation. [ défaut : "Consommation globale" ]</div>
  <div>nameExport : Nom de la courbe Exportation. [ défaut : "Utilisation réseau" ]</div>
  <div>nameBatterie : Nom de la courbe Batterie. [ défaut : "Utilisation batterie" ]</div>
  <div>colorProduction : Couleur de la courbe Production. [ défaut : "#01b4de" ]</div>
  <div>colorConso : Couleur de la courbe Consommation. [ défaut : "#f37320" ]</div>
  <div>colorExport : Couleur de la courbe Exportation. [ défaut : "#6c7073" ]</div>
  <div>colorBatterie : Couleur de la courbe Batterie. [ défaut : "#008000" ]</div>
  <div>titleColor : Couleur du titre et des commandes. [ défaut : "#ffffff" ]</div>
  <div>headerBgColor : Couleur de fond du bandeau supérieur. [ défaut : "rgba(0,0,0,0.4)" ]</div>
  <div>tableHeight : Hauteur du tableau des données en pixels. [ défaut : 200 ]</div>
  <div>defaultDuree : Durée par défaut pour le graphique. 1 = 24h, 2 = J-1 à 5h. [défaut : 2]</div>
  <div>stackWidth : Finesse du graphique en minutes 2,5,10,15,20,30 ou 60 [défaut : 15]</div>
</template>

<script>
// --- Charger les modules Highcharts uniquement si pas déjà présents dans la page
const scripts = [
  '../3rdparty/highstock/modules/exporting.js',   // Permet export CSV / PNG / PDF
  '../3rdparty/highstock/modules/export-data.js', // Permet l'export des données
  '../3rdparty/highstock/modules/accessibility.js' // Accessibilité (lecture d'écran)
];
scripts.forEach(src => {
  if (!document.querySelector(`script[src="${src}"]`)) {
    const s = document.createElement('script');
    s.src = src;
    document.head.appendChild(s);
  }
});
</script>

<style>
/* --- Styles généraux du widget --- */

/* Styles boutons et radio */
/* Bouton radio pour choisir l'horizon */
input[type="radio"] {
  accent-color:#01b4de; 
  transform:scale(1.2);
  margin-right:5px;
}

/* Bouton + et - */
button[id^="btn"] {
  background-color:#01b4de;
  color:white;
  border:none;
  border-radius:50%!important;
  width:28px;
  height:28px;
  cursor:pointer;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  transition:0.2s;
}
button[id^="btn"]:hover { background-color:#028bb0; }
button[id^="btn"]:active { background-color:#016f8b; }

/* disabled style for nav next button */
button[id^="btn"][disabled] {
  opacity:0.45;
  pointer-events:none;
  cursor:default;
}

/* Sélecteur du type de graphique */
select#graphTypeSelect_#id# {
  margin-left:10px;
  padding:4px 6px;
  border-radius:8px!important;
  border:1px solid #ccc;
  width:auto;
  max-width:140px;
  background-color:#01b4de!important;
  color:#fff;
}

/* --- Tableau des données Highcharts --- */
.highcharts-data-table table {
  display: block;
  overflow-y: auto;
  width: 100%;
  border-collapse: collapse;
  box-sizing: border-box;
  padding-right: 12px;
}

.highcharts-data-table thead tr {
  display: table; width: 100%; table-layout: fixed;
  position: sticky; top: 0;
  background: #1F7AE0; color: white; z-index: 2;
}

.highcharts-data-table tbody tr {
  display: table; width: 100%; table-layout: fixed;
  transition: background-color 0.18s ease, color 0.18s ease;
}

.highcharts-data-table th, .highcharts-data-table td {
  text-align: center;
  vertical-align: middle;
  padding: 4px 8px;
  white-space: wrap;
}

/* --- Nouveau layout du header --- */

/* Conteneur général */
#graphHeader_#id# {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 8px 12px;
  box-sizing: border-box;
}

/* Ligne du titre */
.titleRow_#id# {
  width: 100%;
  margin-bottom: 6px;
}

.title_#id# {
  font-size: 18px;
  font-weight: bold;
  text-align: left;
}

/* Ligne commandes + navigation */
.bottomRow_#id# {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: nowrap;
}

/* Commandes à gauche */
.controlsLeft_#id# {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: nowrap;
}

/* Navigation à droite */
.navRight_#id# {
  display: flex;
  align-items: center;
  gap: 8px;
}

#dateDisplay_#id# {
  white-space: nowrap;
}
</style>


<!-- Structure principale du widget -->
<!-- --- Conteneur principal du widget graphique --- -->
<div id="graphContainer_#id#" 
     style="width:100%; height:100%; min-height:300px; position:relative;">
  <!-- width/height à 100% pour remplir le conteneur parent, min-height pour garder une hauteur minimale -->
  <!-- position: relative permet de positionner des éléments enfants de manière absolue si nécessaire -->

  <!-- --- Header du graphique (titre + contrôles) --- -->
<div id="graphHeader_#id#" class="graphHeader">

  <!-- Ligne 1 : le titre -->
  <div class="titleRow_#id#">
    <div id="graphTitle_#id#" class="title_#id#">#title#</div>
  </div>

  <!-- Ligne 2 : commandes à gauche, navigation à droite -->
  <div class="bottomRow_#id#">

    <!-- Commandes à gauche -->
    <div class="controlsLeft_#id#">

      <!-- Durée -->
      <label><input type="radio" name="duree_#id#" value="1"> 24h</label>
      <label style="margin-left:10px;">
        <input type="radio" name="duree_#id#" value="2"> J-1 à 5h
      </label>

      <!-- Type de graphique -->
      <select id="graphTypeSelect_#id#">
        <option value="line">line</option>
        <option value="spline">spline</option>
        <option value="area">area</option>
        <option value="areaspline">areaspline</option>
        <option value="column">column</option>
        <option value="bar">bar</option>
        <option value="polygon">polygon</option>
      </select>

      <!-- StackWidth -->
      <button id="btnMoins_#id#">−</button>
      <span id="stackWidthDisplay_#id#">#stackWidth#</span> min
      <button id="btnPlus_#id#">+</button>

    </div>

    <!-- Navigation à droite -->
    <div class="navRight_#id#">
      <button id="btnPrev_#id#" class="navBtn_#id#">−</button>
      <span id="dateDisplay_#id#">#dateDisplay#</span>
      <button id="btnNext_#id#" class="navBtn_#id#">+</button>

      <button id="btnToday_#id#" class="navBtn_#id#" style="margin-left:8px;">
        <i class="fa fa-home"></i>
      </button>
    </div>

  </div>

</div>

  
  
  
  
  
  
  <!-- --- Conteneur du graphique Highcharts --- -->
  <div id="container_#id#" style="width:100%; height:calc(100% - 60px); margin-top:0px;"></div>
  <!-- width à 100% pour remplir horizontalement, height réduit de 60px pour laisser place au header -->
</div>


<script>
(() => {
  const widgetId = "#id#";

  // --- Helper pour récupérer un élément du DOM avec l'ID complet
  const getEl = id => document.getElementById(id + "_" + widgetId);

  // --- Helper pour retourner une valeur définie ou une valeur par défaut
  const valeurDefinie = (val, def) => (val === undefined || val === "" || /^#.*#$/.test(val)) ? def : val;

  /* --- Paramètres du widget --- */
  const TITLE_ELECTRICITE = valeurDefinie("#title#", "Electricité");  // titre graphique
  const TITLE_COLOR       = valeurDefinie("#titleColor#", "#ffffff"); // couleur titre
  const HEADER_BG_COLOR   = valeurDefinie("#headerBgColor#", "rgba(0,0,0,0.4)"); // fond header
  const DEFAULT_DUREE     = parseInt(valeurDefinie("#defaultDuree#", "2"),10); // durée par défaut
  let stackWidth          = parseInt(valeurDefinie("#stackWidth#", "15"),10); // largeur du bucket en minutes
  let GRAPH_TYPE          = valeurDefinie("#graphType#", "column"); // type de graphique

  // --- IDs commandes Jeedom
  const ID_PRODUCTION   = parseInt(valeurDefinie("#idProduction#", "0"),10);
  const ID_CONSOMMATION = parseInt(valeurDefinie("#idConso#", "0"),10);
  const ID_EXPORTATION  = parseInt(valeurDefinie("#idExport#", "0"),10);
  const ID_BATTERIE     = parseInt(valeurDefinie("#idBatterie#", "0"),10);

  // --- Noms séries
  const NAME_PRODUCTION   = valeurDefinie("#nameProduction#", "Production solaire");
  const NAME_CONSOMMATION = valeurDefinie("#nameConso#", "Consommation globale");
  const NAME_EXPORTATION  = valeurDefinie("#nameExport#", "Utilisation réseau");
  const NAME_BATTERIE     = valeurDefinie("#nameBatterie#", "Utilisation batterie");

  // --- Couleurs séries
  const COLOR_PRODUCTION   = valeurDefinie("#colorProduction#", "#01b4de");
  const COLOR_CONSOMMATION = valeurDefinie("#colorConso#", "#f37320");
  const COLOR_EXPORTATION  = valeurDefinie("#colorExport#", "#6c7073");
  const COLOR_BATTERIE     = valeurDefinie("#colorBatterie#", "#008000");

  const TABLE_HEIGHT = parseInt(valeurDefinie("#tableHeight#", "200"),10); // hauteur tableau

  const stackSteps = [2,5,10,15,20,30,60]; // valeurs possibles de stackWidth
  let duree = DEFAULT_DUREE; // durée active

  // --- Offset en jours pour naviguer dans le temps (0 = aujourd'hui)
  let offsetDays = 0;

  /* --- Fonction pour changer la finesse du graphe (stackWidth) --- */
  function changerStack(direction) {
    const index = stackSteps.indexOf(stackWidth);
    const newIndex = Math.max(0, Math.min(stackSteps.length - 1, index + direction));
    if (newIndex !== index) {
      stackWidth = stackSteps[newIndex];
      getEl("stackWidthDisplay").textContent = stackWidth; // mise à jour affichage
      chargerDonnees(); // recharge graphique avec nouvelle finesse
    }
  }

  /* --- Convertit timestamp JS en format compatible Jeedom --- */
  const formatDateJeedom = ts => {
    const d = new Date(ts), pad = n => String(n).padStart(2,"0");
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  };

  /* --- Format d'affichage demandé : "Du 11/02/2025 05:00 au 12/02/2025 05:00" --- */
  function formatAffichageRange(startTs, endTs) {
    const d1 = new Date(startTs);
    const d2 = new Date(endTs);
    const pad = n => String(n).padStart(2,"0");
    const formatDate = d => `${pad(d.getDate())}/${pad(d.getMonth()+1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
    return `Du ${formatDate(d1)} au ${formatDate(d2)}`;
  }

  /* --- Met à jour l'état des boutons de navigation (désactive → si on est sur aujourd'hui) --- */
  function updateNavButtons() {
    const btnNext = getEl("btnNext");
    if (!btnNext) return;
    if (offsetDays >= 0) {
      // on est au maximum aujourd'hui -> désactiver next
      btnNext.disabled = true;
    } else {
      btnNext.disabled = false;
    }
  }

  /* --- Récupère les données Jeedom et prépare les séries --- */
  function chargerDonnees() {
    const now = new Date();

    // appliquer le décalage en jours (offsetDays) à la date de fin
    const endTime = now.getTime() + offsetDays * 24 * 60 * 60 * 1000;

    // calcul date de début selon durée choisie
    let startTime;
    if (duree === 1) {
      // 24h
      startTime = endTime - 24*60*60*1000;
    } else {
      // J-1 à 5h basé sur la date de endTime
      const d = new Date(endTime);
      startTime = new Date(d.getFullYear(), d.getMonth(), d.getDate()-1, 5,0,0,0).getTime();
    }

    // Mettre à jour l'affichage de la plage (entre les flèches)
    const dateDisplayEl = getEl("dateDisplay");
    if (dateDisplayEl) {
      dateDisplayEl.textContent = formatAffichageRange(startTime, endTime);
    }

    // mettre à jour l'état des boutons (ex : désactiver next si offsetDays >= 0)
    updateNavButtons();

    const puissances = { prod:new Map(), cons:new Map(), exp:new Map(), imp:new Map() };
    const arrondiXmin = ts => Math.floor(ts/(stackWidth*60*1000))*(stackWidth*60*1000); // arrondi timestamps
    let chargements = 0;

    // Map des commandes et facteur (certaines valeurs négatives pour inversion)
    const commandes = {
      [ID_PRODUCTION]:   {map:puissances.prod, facteur:1},
      [ID_CONSOMMATION]: {map:puissances.cons, facteur:-1},
      [ID_EXPORTATION]:  {map:puissances.exp,  facteur:-1},
      [ID_BATTERIE]:     {map:puissances.imp,  facteur:1}
    };

    // --- Récupération des historiques Jeedom
    Object.entries(commandes).forEach(([cmd_id,obj])=>{
      jeedom.history.get({
        cmd_id:parseInt(cmd_id),
        dateStart:formatDateJeedom(startTime),
        dateEnd:formatDateJeedom(endTime),
        success: result=>{
          result.data.forEach(entry => {
  			const t = arrondiXmin(entry[0] + new Date().getTimezoneOffset() * 60000);
  			let val = obj.facteur * entry[1];
			  // Si c'est la batterie (ID_BATTERIE) et que la valeur est négative (charge) → on ignore pour ne pas l'ajouter à la consommation (déjà incluse)
  			if (parseInt(cmd_id) === ID_BATTERIE && val < 0) return;
			const bucket = obj.map.get(t) || { sum: 0, count: 0 };
  			bucket.sum += val;
  			bucket.count++;
  			obj.map.set(t, bucket);
			});
          if(++chargements===4) dessinerGraphique(startTime,endTime,puissances);
        }
      });
    });
  }

 
  // --- Fonction dessin du graphique Highcharts ---
	// Paramètres :
	// startTime / endTime : timestamps début/fin du graphique
	// puissances : objet contenant les données pour prod, conso, export, batterie
  function dessinerGraphique(startTime,endTime,puissances){
   // --- Regroupe tous les timestamps uniques des différentes séries et les trie
    const allTimestamps = Array.from(new Set([
      ...puissances.prod.keys(), 
      ...puissances.cons.keys(),
      ...puissances.exp.keys(), 
      ...puissances.imp.keys()
    ])).sort((a,b)=>a-b);

    // --- Fonction pour calculer la moyenne d’un bucket (si count > 0)
    const getAvg = b => (b && b.count)? b.sum/b.count : 0; // moyenne d’un bucket
    // --- Initialisation des séries pour Highcharts
    const serie_prod=[], serie_imp=[], serie_cons=[], serie_exp=[];
	// --- Construction des séries pour chaque timestamp
    allTimestamps.forEach(ts=>{
   	const avgProd = getAvg(puissances.prod.get(ts));   // moyenne production
    const avgImp  = getAvg(puissances.imp.get(ts));    // moyenne batterie
    const avgCons = getAvg(puissances.cons.get(ts));   // moyenne consommation
    const avgExp  = getAvg(puissances.exp.get(ts));    // moyenne export réseau
    const net = avgProd + avgImp + avgCons + avgExp;	// calcul du net
      let correctedExp = avgExp; if(Math.abs(net)>0.5) correctedExp -= net; // corrige les éventuels écarts

      // Ajout des valeurs dans les séries
      serie_prod.push([ts,Math.round(avgProd)]);
      serie_imp.push([ts,Math.round(avgImp)]);
      serie_cons.push([ts,Math.round(avgCons)]);
      serie_exp.push([ts,Math.round(correctedExp)]);
    });

    // --- Configuration globale Highcharts
    Highcharts.setOptions({global:{useUTC:false}});
    
  	// --- Création du graphique
    const chart = Highcharts.chart("container_"+widgetId,{
      chart:{type:GRAPH_TYPE,height:null}, // type choisi par l'utilisateur
      title:{text:""}, // pas de titre dans le graphique
      // axe horizontal définition de l'affichage
   	xAxis: {
      type: "datetime", 
      min: startTime, 
      max: endTime, 
      tickInterval: 3600000,                    // intervalle d'une heure
      labels: { format:"{value:%H}h" }          // format HHh
    },
        // axe vertical - ne pas afficher
	yAxis: {
      visible: false, 
      startOnTick: false, 
      endOnTick: false, 
      title: { text: "Puissance (W)" }
    },
      // --- Tooltip : affiche les infos détaillées au survol d'un point
      tooltip: {
        xDateFormat: "%H:%M",
        shared: true,
        formatter: function () {
          const pointStart = this.x, pointEnd = pointStart + stackWidth * 60000;
          const f = ts => Highcharts.dateFormat("%H:%M", ts);
          let s = `<b>${f(pointStart)} → ${f(pointEnd)}</b><br/>`;

          // Trier les points pour afficher "Consommation" en premier
          const sortedPoints = [];
          let consoPoint = null;

          this.points.forEach(p => {
            if (p.series.name.toLowerCase().includes("conso")) consoPoint = p;
            else sortedPoints.push(p);
          });

          // --- Affichage consommation d'abord
          if (consoPoint) {
            const val = Math.abs(consoPoint.y);
            if (val !== 0)
              s += `<span style="color:${consoPoint.series.color}">●</span> ${consoPoint.series.name}: <b>${Math.trunc(val)}</b><br/>`;
          }

          // --- Séparateur
          s += `<br/><b>Sources :</b><br/>`;

          // --- Puis les autres (production, batterie, export, etc.)
          sortedPoints.forEach(p => {
            const val = Math.abs(p.y);
            if (val !== 0)
              s += `<span style="color:${p.series.color}">●</span> ${p.series.name}: <b>${Math.trunc(val)}</b><br/>`;
          });

          return s;
        }
      },
       // --- Options spécifiques pour les séries de type "column" (colonnes)
      plotOptions:{column:{stacking:"normal",groupPadding:0.1,pointPadding:0,borderWidth:0}},
      
      // --- Active les options d'export du graphique
	  // Permet à l'utilisateur de télécharger le graphique en PNG, PDF ou CSV
      exporting:{enabled:true},
     
      // --- Chaque objet dans "series" correspond à une série de données du graphique
	  // Les séries sont empilées (stack: "Puissance") pour que la somme des valeurs soit visible
      // Les couleurs et noms sont personnalisables pour une lecture plus intuitive
      series:[
        {name:NAME_EXPORTATION,data:serie_exp,stack:"Puissance",color:COLOR_EXPORTATION},
        {name:NAME_BATTERIE,data:serie_imp,stack:"Puissance",color:COLOR_BATTERIE},
        {name:NAME_PRODUCTION,data:serie_prod,stack:"Puissance",color:COLOR_PRODUCTION},
        {name:NAME_CONSOMMATION,data:serie_cons,stack:"Puissance",color:COLOR_CONSOMMATION}
      ]
    });

  
	// --- Gestion du tableau des données et ajout d'un bouton pour le fermer
Highcharts.addEvent(chart, 'afterViewData', function () {
    const tableDiv = this.dataTableDiv;
    if (!tableDiv || document.getElementById(`closeTable_${widgetId}`)) return;

    // --- Créer le wrapper du tableau ---
    const wrapper = document.createElement("div");
    wrapper.style.position = "relative";
    wrapper.style.width = "100%";
    wrapper.style.height = "100%";
    wrapper.style.display = "flex";
    wrapper.style.flexDirection = "column";

    // --- Déplacer le tableau existant dans le wrapper ---
    tableDiv.parentNode.insertBefore(wrapper, tableDiv);
    wrapper.appendChild(tableDiv);

    // --- Créer le bouton ---
    const closeBtn = document.createElement("div");
    closeBtn.id = `closeTable_${widgetId}`;
    closeBtn.textContent = "×";
    closeBtn.title = "Fermer le tableau et revenir au graphique";
    closeBtn.style.cssText = `
        font-size: 18px; font-weight: bold; color: #fff;
        background: #01b4de; border-radius: 50%;
        width: 24px; height: 24px; line-height: 22px;
        text-align: center; cursor: pointer;
        align-self: flex-end; margin-bottom: 4px;
    `;
    closeBtn.addEventListener("click", () => {
        chart.hideData();
        closeBtn.remove();
    });

    // --- Ajouter le bouton en haut du wrapper ---
    wrapper.insertBefore(closeBtn, tableDiv);

    // --- S'assurer que le tableau a l'ascenseur et prend tout l'espace restant ---
    tableDiv.style.flex = "1 1 auto";   // prend l’espace restant
    tableDiv.style.overflowY = "auto";  // scroll vertical
});

   
    
    
    
	// --- Stocke l'objet chart dans le conteneur pour un accès ultérieur
	getEl("container").chart = chart;

	// --- Ajuste la hauteur du graphique selon la taille du header
	ajusterHauteur();
  	}

  // --- Ajuste la hauteur du graphique en fonction du header
  function ajusterHauteur(){
    const header = getEl("graphHeader");
    const container = getEl("container");
    const totalHeight = getEl("graphContainer").offsetHeight;
    const headerHeight = header.offsetHeight;
    container.style.height = (totalHeight - headerHeight - 0) + "px"; 
  }

  // --- Initialisation header et contrôles ---
  const header = getEl("graphHeader");
  header.style.backgroundColor = HEADER_BG_COLOR;
  header.style.color = TITLE_COLOR;
  getEl("graphTitle").style.color = TITLE_COLOR;

  // --- Gestion interactions utilisateur
  document.querySelectorAll(`input[name="duree_${widgetId}"]`).forEach(radio=>{
    radio.addEventListener("change", function(){
      duree = parseInt(this.value);
      chargerDonnees();
    });
  });
  getEl("btnMoins").addEventListener("click",()=>changerStack(-1));
  getEl("btnPlus").addEventListener("click",()=>changerStack(1));
  const graphSelect = getEl("graphTypeSelect");
  if(graphSelect){
    graphSelect.addEventListener("change",()=>{ GRAPH_TYPE = graphSelect.value; chargerDonnees(); });
  }

  // --- Navigation temporelle évènements pour les flèches et aujourd'hui
  const btnPrevEl = getEl("btnPrev");
  const btnNextEl = getEl("btnNext");
  const btnTodayEl = getEl("btnToday");

  if (btnPrevEl) {
    btnPrevEl.addEventListener("click", () => {
      offsetDays -= 1;
      updateNavButtons();
      chargerDonnees();
    });
  }

  if (btnNextEl) {
    btnNextEl.addEventListener("click", () => {
      // n'autorise pas d'aller au-delà d'aujourd'hui (offsetDays >= 0)
      if (offsetDays < 0) {
        offsetDays += 1;
        updateNavButtons();
        chargerDonnees();
      }
    });
  }

  if (btnTodayEl) {
    btnTodayEl.addEventListener("click", () => {
      offsetDays = 0;
      updateNavButtons();
      chargerDonnees();
    });
  }

  // --- Valeurs initiales
  document.querySelector(`input[name="duree_${widgetId}"][value="${duree}"]`).checked = true;
  getEl("stackWidthDisplay").textContent = stackWidth;
  if (graphSelect) graphSelect.value = GRAPH_TYPE;
  // mise à jour initiale état boutons nav
  updateNavButtons();
  chargerDonnees();

  // --- Fixe la hauteur max du tableau de données
  (function injectTableHeightStyle(){
    const id = "tableHeightStyle_" + widgetId;
    if(document.getElementById(id)) return;
    const s = document.createElement("style");
    s.id = id;
    s.textContent = `
      #graphContainer_${widgetId} .highcharts-data-table table {
        max-height: ${TABLE_HEIGHT}px;
      }
    `;
    document.head.appendChild(s);
  })();

  // --- Ajuste dynamiquement la hauteur du graphique lorsque la fenêtre est redimensionnée
	window.addEventListener("resize", ajusterHauteur);

})();
</script>
1 « J'aime »