📈 Widget Graphique sur IPX800 V5 avec Jeedom

Bonjour Ă  tous,
il y a quelques temps j’avais rĂ©digĂ© un tutoriel montrant comment utiliser Jeedom en Data-logger.

:bar_chart: Jeedom : data-logger pour IPX800

Ce nouveau tutoriel vous montrera comment lire les donnĂ©es historisĂ©es sur Jeedom pour afficher un Graphique sur Dashboard ou Liveview de l’IPX800 V5.

widget-graph

Ce rĂ©sultat a nĂ©cessitĂ© un gros travail de recherche et d’optimisation, c’est pourquoi je vous le partage ici.

Nous aurons besoin du plugin Data Export pour générer un fichier CSV avec les datas.
Nous aurons Ă©galement besoin du plugin Script pour envoyer le contenu du CSV sous forme de Json, Ă  l’initiative de l’IPX800.

Le Plugin Data Export

  1. Connectez-vous Ă  Jeedom : Ouvrez votre interface Jeedom dans un navigateur.
  2. AccĂ©dez Ă  la gestion des plugins : Allez dans le menu Plugins → Gestion des plugins.
  3. Installez le plugin Data Export :
  4. activez le plugin.

Configurer le plugin Data Export

  1. AccĂ©dez Ă  la configuration du plugin : Allez dans Plugins → Data Export → Configuration.

Cochez la case Horodatage pour exporter les dates au format Timestamp.

  1. Activez la commande puis configurez la pĂ©riode durant laquelle les datas seront extraites. Personnellement j’extrais les donnĂ©es sur 1 jour pour avoir un graphe rapide sur IPX800.
    Activez l’Envoi programmĂ© et programmez la rĂ©currence

  2. SĂ©lectionnez la commande Ă  extraire
    Cliquez sur image puis sélectionnez la donnée à extraire. Pensez à valider en haut de la liste.
    Mon tutoriel concerne l’extraction d’une donnĂ©e unique, veillez Ă  ne sĂ©lectionner qu’une seule commande dans la liste.

  3. Allez sur l’onglet Archivage
    Renseignez la rétention des fichiers CSV. Personnellement je ne conserve que le dernier fichier généré par le script. Il est donc remplacé toutes les 5minutes selon ma configuration CRON.

  4. Exécutez un export CSV
    Cliquez sur le bouton ![image|117x38,50%] pour générer le premier fichier csv.


    Si vous sélectionnez le fichier généré, vous pourrez vérifier son contenu

  5. Cliquez sur le bouton image et relevez l’ID de l’export dont vous aurez besoin ultĂ©rieurement.


    Dans notre exemple l’id est 38

Le plugin Script

Nous allons crĂ©er un script en php, que nous exĂ©cuterons directement Ă  partir de son URL via le serveur Web de Jeedom. Nous utiliserons pour cela le plugin Script qui nous facilitera la mise en place du script et la gestion des droits d’accĂšs.

Installez le plugin depuis le Market. Pensez à l’activer.

  1. Ajoutez un Ă©quipement en cliquant sur le bouton image

  2. Donnez un nom Ă  l’équipement

  3. Rattachez l’équipement Ă  un Dashboard (parent) activez le puis rendez le visible.

  4. Cliquez sur le bouton image
    configurez
    comme suit

Cliquez sur le crayon au dessus du champ requĂȘte afin d’ouvrir l’éditeur de script.
Ajoutez un nouveau fichier nommĂ© json.php puis collez ce code dans l’éditeur

<?php
// RĂ©cupĂ©ration du paramĂštre `subfolder` passĂ© dans la requĂȘte
$subfolder = isset($_GET['subfolder']) ? $_GET['subfolder'] : '32';

// DĂ©finir le chemin vers le dossier DataExport
$basePath = realpath('/var/www/html/plugins/dataexport/data');
$folderPath = $basePath . '/' . $subfolder;

// VĂ©rifier si le dossier existe
if (!is_dir($folderPath)) {
    echo json_encode(['error' => "Dossier inexistant : $subfolder"]);
    return;
}

// Trouver le fichier CSV le plus récent
$latestFile = '';
$latestTime = 0;
foreach (glob($folderPath . '/*.csv') as $file) {
    if (filemtime($file) > $latestTime) {
        $latestFile = $file;
        $latestTime = filemtime($file);
    }
}

// Vérification si un fichier a été trouvé
if (!$latestFile) {
    echo json_encode(['error' => 'Aucun fichier CSV trouvé']);
    return;
}

// Lecture du contenu du fichier CSV et conversion en JSON
$data = [];
if (($handle = fopen($latestFile, 'r')) !== false) {
    $headers = fgetcsv($handle, 0, ';'); // Lire les en-tĂȘtes
    if ($headers === false) {
        echo json_encode(['error' => 'Impossible de lire les en-tĂȘtes du fichier CSV']);
        return;
    }

    // Supprimer le BOM des en-tĂȘtes
    $headers[0] = preg_replace('/\x{FEFF}/u', '', $headers[0]);

    while (($row = fgetcsv($handle, 0, ';')) !== false) {
        $row = array_map(function ($value) {
            return str_replace(',', '.', $value); // Convertit ',' en '.'
        }, $row);
        $rowData = array_combine($headers, $row);

        if (isset($rowData['Date']) && isset($rowData['Commande']) && isset($rowData['Valeur']) && isset($rowData['Unité'])) {
            $rowData['Date'] = intval($rowData['Date']); // Convertir le timestamp en entier
            $rowData['Valeur'] = floatval($rowData['Valeur']); // Convertir la valeur en nombre flottant
            $data[] = [
                'Date' => $rowData['Date'],
                'Commande' => $rowData['Commande'],
                'Valeur' => $rowData['Valeur'],
                'Unité' => $rowData['Unité']
            ];
        }
    }
    fclose($handle);
} else {
    echo json_encode(['error' => 'Impossible d\'ouvrir le fichier CSV']);
    return;
}

// DĂ©finir l'en-tĂȘte Content-Type pour le texte brut
header('Content-Type: text/plain; charset=ISO-8859-1');

// Vérifier si des données ont été trouvées
if (empty($data)) {
    echo json_encode(['error' => 'Aucune donnée trouvée dans le fichier CSV']);
    return;
}

// Retourner les données en JSON encodé en texte brut
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
?>

Le script peut traiter n’importe quel csv du plugin Data Export car il reçoit un ID en paramĂštre dans son URL. Il rĂ©cupĂšre le nom du fichier csv le plus rĂ©cent dans un sous-dossier de plugin\script\dataexport\data puis convertit son contenu au format Json.
Un seul script suffit, nul besoin d’en crĂ©er plusieurs mĂȘme si vous crĂ©ez plusieurs Ă©quipements Data Exports.

Revenez sur l’onglet Ă©quipement puis sĂ©lectionnez votre script en cliquant sur l’icone â€č Dossier â€ș au dessus du champ â€č RequĂȘte â€ș.

Cliquez sur le bouton â€č configuration avancĂ©e â€ș puis relevez l’id du script (39 dans notre exemple)

Remarque : dans le code php, j’ai renseignĂ© '32' comme valeur par dĂ©faut Ă  la variable subfolder. C’est l’id de mon export, cela me permet de vĂ©rifier le bon fonctionnement Ă  partir du dashboard Jeedom directement (sans Tag dans l’URL).

Les CORS
Testez le script en entrant son URL dans votre navigateur :
http://<IP_JEEDOM>/plugins/script/data/json.php?subfolder=<subfolder>
Remplacez <IP_JEEDOM> par l’adresse IP de votre Jeedom, puis la variable <subfolder> par l’id de l’équipement Data Export relevĂ© prĂ©cĂ©demment (32 dans mon cas).
Vous devriez obtenir des données json comme ceci:

[
    {
        "Date": 1734735656,
        "Commande": "TEMP AIR",
        "Valeur": 5.08,
        "Unité": "°C"
    },
    {
        "Date": 1734739256,
        "Commande": "TEMP AIR",
        "Valeur": 5.19,
        "Unité": "°C"
    },
    {
        "Date": 1734742857,
        "Commande": "TEMP AIR",
        "Valeur": 5.246666666,
        "Unité": "°C"
    },
    {
        "Date": 1734746457,
        "Commande": "TEMP AIR",
        "Valeur": 5.1,
        "Unité": "°C"
    }]

Si la page ne retourne pas les données au format JSON, il se peut que le dossier plugin/script/data ait des droits restreints.
Vous devez Ă©diter le fichier .htaccess de ce dossier.
Retournez sur l’onglet Ă©quipement de votre script, cliquez sur l’icone crayon au dessus du champ requĂȘte. Editez le fichier .htaccess et remplacez son contenu par le code suivant:

<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
    Header set Access-Control-Allow-Headers "Content-Type, Authorization"
</IfModule>

Widget simple graphique sur ipx800 V5

  1. Créez un widget de type HTML sur votre dashboard, puis collez ce code:
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    JS

Ce widget initialise la librairie nécessaire à la construction du graphique.
2. créez un autre widget HTML avec ce code:

<script>
    // Variables globales pour la personnalisation
    const jeedomUrl = 'http://<ip_Jeedom>/plugins/script/data/json.php';
    const pointStyle = 'rect';
    const pointRadius = 0.5;
    const pointRadiusModal = 2;
    const pointBackgroundColor = 'red';
    const defaultSubfolder = '32';
    const graphTitle="Température de l'eau";
   
 let modal;

    function convertTimestampToTime(t) {
        const date = new Date(t * 1000);
        const hours = date.getHours().toString().padStart(2, '0');
        const minutes = date.getMinutes().toString().padStart(2, '0');
        const seconds = date.getSeconds().toString().padStart(2, '0');
        return `${hours}:${minutes}:${seconds}`;
    }

    function convertTimestampToDateTime(t) {
        const date = new Date(t * 1000);
        const year = date.getFullYear();
        const month = (date.getMonth() + 1).toString().padStart(2, '0');
        const day = date.getDate().toString().padStart(2, '0');
        const hours = date.getHours().toString().padStart(2, '0');
        const minutes = date.getMinutes().toString().padStart(2, '0');
        const seconds = date.getSeconds().toString().padStart(2, '0');
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    async function startScenarioAndGetVariable(d) {
        const subfolder = d; // Sous-dossier passé en paramÚtre

        // Construire l'URL pour appeler directement le script qui génÚre le JSON
        const startScriptUrl = `${jeedomUrl}?subfolder=${encodeURIComponent(subfolder)}`;

        try {
            // Appeler le script qui gĂ©nĂšre le JSON avec les en-tĂȘtes CORS
            const response = await fetch(startScriptUrl, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                mode: 'cors'
            });
            if (!response.ok) {
                throw new Error('Erreur lors de l\'exécution du scénario');
            }

            // Extraire le contenu JSON directement depuis la réponse
            const jsonData = await response.json();

            // Vérification des données
            if (!Array.isArray(jsonData) || jsonData.length === 0) {
                throw new Error('Aucune donnée trouvée dans le JSON');
            }

            // Vérifier si la propriété 'Date' existe dans la premiÚre ligne
            if (!jsonData[0].hasOwnProperty('Date')) {
                throw new Error('La propriété "Date" est absente dans le JSON');
            }

            // Trier les données par date
            jsonData.sort((a, b) => parseInt(a.Date, 10) - parseInt(b.Date, 10));

            // Afficher les données sous forme de graphique
            displayGraph(jsonData);
        } catch (error) {
            alert(`Erreur : ${error.message}`);
        }
    }

    function displayGraph(data) {
        // Extraire les labels (dates) et les valeurs
        const labels = data.map(item => {
            const timestamp = parseInt(item.Date, 10); // Convertir le timestamp Unix en millisecondes
            return convertTimestampToTime(timestamp);
        });

        const values = data.map(item => {
            const valeur = item.Valeur.toString().replace(',', '.'); // Convertir en chaĂźne et remplacer ',' par '.'
            return parseFloat(valeur);
        });

        // Créer le graphique
        const ctx = document.getElementById('dataChart').getContext('2d');
        graph = new Chart(ctx, {
            type: 'line',
            data: {
                labels: labels,
                datasets: [{
                    label: graphTitle, // Utiliser le texte de l'option sélectionnée comme titre du graphique
                    data: values,
                    borderColor: 'blue',
                    borderWidth: 0.3,
                    fill: true,
                    backgroundColor:  'rgba(255, 255, 0, 0.7)',
                    pointRadius: pointRadius,
                    pointStyle: pointStyle,
                    pointBackgroundColor: pointBackgroundColor
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        display: false
                    },
                    title: {
                        display: true,
                        text: graphTitle, // Utiliser le texte de l'option sélectionnée comme titre du graphique
                        color:'blue',
                        font: { size: 18 // Taille de police du titre
                        }
                    }
                },
                scales: {
                    x: {
                        title: {
                            display: false
                        },
                        grid: { color: 'rgba(211, 211, 211, 0.6)' // Couleur de la grille en RGBA 
                        },
                        ticks: {
                            color: 'blue'
                        }
                    },
                    y: {
                        title: {
                            display: false
                        },
                        grid: { color: 'rgba(211, 211, 211, 0.6)' // Couleur de la grille en RGBA 
                        },
                        ticks: {
                            color: 'blue'
                        }
                    }
                }
            }
        });


        // Ajouter un Ă©vĂ©nement de clic pour ouvrir la fenĂȘtre modale
        document.getElementById('dataChart').addEventListener('click', function() {
            openModal(data);
        });
    }

    function openModal(data) {
        if (modal) {
            document.body.removeChild(modal);
        }

        const labels = data.map(item => {
            const timestamp = parseInt(item.Date, 10);
            return convertTimestampToDateTime(timestamp);
        });

        const values = data.map(item => {
            const valeur = item.Valeur.toString().replace(',', '.');
            return parseFloat(valeur);
        });
        // CrĂ©er la fenĂȘtre modale
        modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '0';
        modal.style.left = '0';
        modal.style.width = '100%';
        modal.style.height = '100%';
        modal.style.backgroundColor = 'rgba(0, 0, 0, 0.95)'; // Diminuer la transparence
        modal.style.display = 'flex';
        modal.style.justifyContent = 'center';
        modal.style.alignItems = 'center';
        modal.style.zIndex = '1000';

        // CrĂ©er le canvas pour le graphique dans la fenĂȘtre modale
        const modalCanvas = document.createElement('canvas');
        modalCanvas.id = 'modalChart';
        modalCanvas.width = 1200;
        modalCanvas.height = 600;
        modal.appendChild(modalCanvas);

        // Ajouter la fenĂȘtre modale au document
        document.body.appendChild(modal);

        // CrĂ©er le graphique dans la fenĂȘtre modale
        const modalCtx = modalCanvas.getContext('2d');
        new Chart(modalCtx, {
            type: 'line',
            data: {
                labels: labels,
                datasets: [{
                    label: '', // Supprimer le titre du graphique
                    data: values,
                    borderColor: 'rgba(75, 192, 192, 1)',
                    borderWidth: 1, // Lignes fines
                    fill: true,
                    pointRadius: pointRadiusModal,
                    pointStyle: pointStyle,
                    pointBackgroundColor: pointBackgroundColor
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        display: false // Supprimer la légende
                    },
                },
                scales: {
                    x: {
                        title: {
                            display: false // Supprimer le titre de l'axe X
                        },
                        ticks: {
                            color: 'white' // Mettre les heures en blanc
                        }
                    },
                    y: {
                        title: {
                            display: false // Supprimer le titre de l'axe Y
                        }
                    }
                }
            }
        });

        // Ajouter un Ă©vĂ©nement de clic pour fermer la fenĂȘtre modale
        modal.addEventListener('click', function() {
            document.body.removeChild(modal);
        });
    }

    // Lancer la récupération des données pour un sous-dossier spécifique
    startScenarioAndGetVariable(defaultSubfolder);
</script>

<meta charset="UTF-8">

<canvas id="dataChart" width="100%" height="100%"></canvas>

Modifiez ces 7 lignes de code

    // Variables globales pour la personnalisation
    const jeedomUrl = 'http://<ip_Jeedom>/plugins/script/data/json.php';
    const pointStyle = 'rect';
    const pointRadius = 0.5;
    const pointRadiusModal = 2;
    const pointBackgroundColor = 'red';
    const defaultSubfolder = '32';
    const graphTitle="Température de l'eau";

Renseignez les variables globales :

  • <ip_jeedom> est l’adresse ip de votre installation Jeedom.
  • pointStyle est la variable qui conditionne la forme des repĂšres sur la courbe. Les valeurs possibles peuvent ĂȘtre les suivantes:
    • circle : Un cercle (valeur par dĂ©faut).
    • cross : Une croix.
    • crossRot : Une croix inclinĂ©e Ă  45 degrĂ©s.
    • dash : Un tiret.
    • line : Une ligne horizontale.
    • rect : Un rectangle.
    • rectRounded : Un rectangle avec des coins arrondis.
    • rectRot : Un rectangle inclinĂ© Ă  45 degrĂ©s.
    • star : Une Ă©toile.
    • triangle : Un triangle.
  • pointRadius est la taille du repĂšre sur la courbe
  • pointBackgroundColor est la couleur du repĂšre
  • defaultSubfolder : renseignez l’id du Data Export relevĂ© prĂ©cĂ©demment (â€č 32 â€ș dans mon cas). L’id correspond au nom du sous-dossier crĂ©Ă© par Data Export.

Si vous cliquez sur le graphique, une modale s’ouvre en plein Ă©cran pour afficher le graph.

Widget multi-graphiques

Si vous souhaitez afficher le graphique de plusieurs données, préférez le widget suivant qui permettra la sélection de chaque graphique à afficher (1 à la fois), ne dupliquez pas le widget simple graphique sur le dashboard.
Créez le widget HTML et collez ce code:

<style>
    /* Styles pour le panneau latéral */
    #settings-panel {
        position: absolute;
        top: 20;
        left: 20;
        width: 0;
        height: 100%;
        background-color: rgba(111,111,111,0.8);
        box-shadow: 2px 0 5px rgba(0,0,0,1);
        transition: width 0.3s ease;
        overflow: hidden;
    }

    #settings-panel.open {
        width: 95%;
    }

    .close-btn {
        position: absolute;
        top: 10px;
        right: 10px;
        background-color: transparent;
        border: none;
        font-size: 1.5em;
        cursor: pointer;
    }

    #gear-icon {
        position: absolute;
        top: 10px;
        left: 12px;
        font-size: 1.1em;
        cursor: pointer;
    }

    .form-label {
        font-size: 0.9em;
        margin-left: 1em;
    }

    .form-select, .form-input {
        margin: 10px 0;
        margin-left: 1em;
    }

</style>

<script>
    // Variables globales pour la personnalisation
    const jeedomUrl = 'http://<ip_jeedom>/plugins/script/data/json.php';
    const pointStyle = 'rectRot';
    const pointRadius = 0.5;
    const pointRadiusModal = 2.5;
    const pointBackgroundColor = 'red';
    const defaultSubfolder = '32';
    const sel2Options = [
        { value: '32', text: 'Température eau' },
        { value: '37', text: 'Température AIR' }
    ];


     let graph, subfolder = defaultSubfolder, modal;

    function togglePanel() {
        const panel = document.getElementById('settings-panel');
        panel.classList.toggle('open');
    }

function populateSel2Options() {
    const sel2 = document.getElementById('sel2');
    sel2Options.forEach(option => {
        const opt = document.createElement('option');
        opt.value = option.value;
        opt.text = option.text; sel2.appendChild(opt);
        }); }

 function convertTimestampToTime(t) {
        const date = new Date(t * 1000);
        const hours = date.getHours().toString().padStart(2, '0');
        const minutes = date.getMinutes().toString().padStart(2, '0');
        const seconds = date.getSeconds().toString().padStart(2, '0');
        return `${hours}:${minutes}:${seconds}`;
    }

    function convertTimestampToDateTime(t) {
        const date = new Date(t * 1000);
        const year = date.getFullYear();
        const month = (date.getMonth() + 1).toString().padStart(2, '0');
        const day = date.getDate().toString().padStart(2, '0');
        const hours = date.getHours().toString().padStart(2, '0');
        const minutes = date.getMinutes().toString().padStart(2, '0');
        const seconds = date.getSeconds().toString().padStart(2, '0');
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    async function fetchAndDisplayData() {
        // Récupérer le texte de l'option sélectionnée
        const selectElement = document.getElementById('sel2');
        subfolder = selectElement.options[selectElement.selectedIndex].value;
        const apiUrl = `${jeedomUrl}?subfolder=${encodeURIComponent(subfolder)}`;

        try {
            const response = await fetch(apiUrl, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                }
            });

            if (!response.ok) {
                throw new Error('Erreur lors de la récupération des données');
            }

            const jsonData = await response.json();

            if (!Array.isArray(jsonData) || jsonData.length === 0) {
                throw new Error('Aucune donnée trouvée dans le JSON');
            }

            jsonData.sort((a, b) => parseInt(a.Date, 10) - parseInt(b.Date, 10));

            if (graph) {
                graph.destroy();
            }

            displayGraph(jsonData);
        } catch (error) {
            alert(`Erreur : ${error.message}`);
        }
    }

    function displayGraph(data) {
        const labels = data.map(item => {
            const timestamp = parseInt(item.Date, 10);
            return convertTimestampToTime(timestamp);
        });

        const values = data.map(item => {
            const valeur = item.Valeur.toString().replace(',', '.');
            return parseFloat(valeur);
        });

        // Récupérer le texte de l'option sélectionnée
        const selectElement = document.getElementById('sel2');
        const selectedText = selectElement.options[selectElement.selectedIndex].text;

        const ctx = document.getElementById('dataChart').getContext('2d');
        graph = new Chart(ctx, {
            type: 'line',
            data: {
                labels: labels,
                datasets: [{
                    label: selectedText, // Utiliser le texte de l'option sélectionnée comme titre du graphique
                    data: values,
                    borderColor: 'blue',
                    borderWidth: 0.3,
                    fill: true,
                    backgroundColor:  'rgba(255, 255, 0, 0.7)',
                    pointRadius: pointRadius,
                    pointStyle: pointStyle,
                    pointBackgroundColor: pointBackgroundColor
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        display: false
                    },
                    title: {
                        display: true,
                        text: selectedText, // Utiliser le texte de l'option sélectionnée comme titre du graphique
                        color:'blue',
                        font: { size: 18 // Taille de police du titre
                        }
                    }
                },
                scales: {
                    x: {
                        title: {
                            display: false
                        },
                        grid: { color: 'rgba(211, 211, 211, 0.6)' // Couleur de la grille en RGBA 
                        },
                        ticks: {
                            color: 'blue'
                        }
                    },
                    y: {
                        title: {
                            display: false
                        },
                        grid: { color: 'rgba(211, 211, 211, 0.6)' // Couleur de la grille en RGBA 
                        },
                        ticks: {
                            color: 'blue'
                        }
                    }
                }
            }
        });

        // Ajouter un Ă©vĂ©nement de clic pour ouvrir la fenĂȘtre modale
        document.getElementById('dataChart').addEventListener('click', function() {
            openModal(data);
        });
    }

    function openModal(data) {
        // Supprimer la fenĂȘtre modale prĂ©cĂ©dente si elle existe
        if (modal) {
            document.body.removeChild(modal);
        }
        const labels = data.map(item => {
            const timestamp = parseInt(item.Date, 10);
            return convertTimestampToDateTime(timestamp);
        });

        const values = data.map(item => {
            const valeur = item.Valeur.toString().replace(',', '.');
            return parseFloat(valeur);
        });

        // CrĂ©er la nouvelle fenĂȘtre modale
        modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '0';
        modal.style.left = '0';
        modal.style.width = '100%';
        modal.style.height = '100%';
        modal.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; // Diminuer la transparence
        modal.style.display = 'flex';
        modal.style.justifyContent = 'center';
        modal.style.alignItems = 'center';
        modal.style.zIndex = '1000';

        // CrĂ©er le canvas pour le graphique dans la fenĂȘtre modale
        const modalCanvas = document.createElement('canvas');
        modalCanvas.id = 'modalChart';
        modalCanvas.width = '95%';
        modalCanvas.height = '95%';
        modal.appendChild(modalCanvas);

        // Ajouter la fenĂȘtre modale au document
        document.body.appendChild(modal);

        // CrĂ©er le graphique dans la fenĂȘtre modale
        const modalCtx = modalCanvas.getContext('2d');
        new Chart(modalCtx, {
            type: 'line',
            data: {
                labels: labels,
                datasets: [{
                    label: '', // Supprimer le titre du graphique
                    data: values,
                    borderColor: 'rgba(75, 192, 192, 1)',
                    borderWidth: 1, // Lignes fines
                    fill: true,
                    pointRadius: pointRadiusModal,
                    pointStyle: pointStyle,
                    pointBackgroundColor: pointBackgroundColor
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        display: false // Supprimer la légende
                    },
                },
                scales: {
                    x: {
                        title: {
                            display: false // Supprimer le titre de l'axe X
                        },
                        ticks: {
                            color: 'white' // Mettre les heures en blanc
                        }
                    },
                    y: {
                        title: {
                            display: false // Supprimer le titre de l'axe Y
                        }
                    }
                }
            }
        });

        // Ajouter un Ă©vĂ©nement de clic pour fermer la fenĂȘtre modale
        modal.addEventListener('click', function() {
            document.body.removeChild(modal);
            modal = null; // RĂ©initialiser la variable modal
        });
    }

    function SelChange(){
        togglePanel();
        fetchAndDisplayData();
    }

    // Charger le graphique avec la premiÚre valeur par défaut
    setTimeout(populateSel2Options, 300);
    setTimeout(fetchAndDisplayData, 500);
    setInterval(fetchAndDisplayData, 300000); // RafraĂźchir toutes les 5 minutes
</script>




<div id="gear-icon" onclick="togglePanel();">
    ⚙
</div>

<div id="settings-panel">
    <button class="close-btn" onclick="togglePanel();">&times;</button>
    <label class="form-label" for="sel2">Sélectionnez une donnée :</label>
    <select class="form-select" id="sel2" onchange='SelChange()'></select>
</div>

<canvas id="dataChart" width="100%" height="100%"></canvas>
  • Modifiez ces 9 lignes de code (vous trouverez le rĂŽle de chaque variable dans le descriptif plus haut) :
  • La variable sel2Options est constituĂ©e d’un tableau contenant les diffĂ©rents graphiques Ă  afficher. Value prend la valeur de l’id du Data Export pour chacun d’eux.
// Variables globales pour la personnalisation
    const jeedomUrl = 'http://<ip_jeedom>/plugins/script/data/json.php';
    const pointStyle = 'rectRot';
    const pointRadius = 2;
    const pointBackgroundColor = 'blue';
    const defaultSubfolder = '32';
    const sel2Options = [
        { value: '32', text: 'Température eau' },
        { value: '37', text: 'Température AIR' }
    ];

Dans cet exemple, nous souhaitons afficher 2 graphiques. Nous avons donc crĂ©Ă© 2 Data Export, respectivement pour la tempĂ©rature de l’eau et la tempĂ©rature de l’air.
l’attribut value prend l’id du Data Export crĂ©Ă© pour chaque donnĂ©e.

La roue dentĂ©e permet l’ouverture d’un panneau latĂ©ral afin de sĂ©lectionner le graphique Ă  afficher.

Comme prĂ©cĂ©demment, une modale est affichĂ©e lors d’un clic sur le graphique.

Une bulle d’information est affichĂ©e lors du survol de la courbe.

Adaptation au Liveview

les 2 widgets ci-dessus sont compatibles Liveview, ils se crĂ©ent de la mĂȘme façon que sur Dashboard et nĂ©cessitent aussi la mise en place de la librairie graph.js.
Il faut placer donc un widget et y mettre la référence de la librairie graph.js.

widget-graph-liveview

Remarque globale:

j’ai choisi d’afficher des courbes simples, pour l’affichage d’une seule donnĂ©e Ă  la fois, afin de conserver un code lĂ©ger et privilĂ©gier les performances.
Si des graphiques plus complexes (multi-donnĂ©es, fonctions rĂ©capitulatives, 
) sont souhaitĂ©s, Jeedom reste l’outil Ă  utiliser.

J’ai remarquĂ© que parfois certaines valeurs sortaient de la sĂ©quence triĂ©e, je ne sais pas encore d’oĂč cela peut venir. Des glitch dans le csv ? un conflit d’accĂšs aux donnĂ©es CSV (mĂ j par Jeedom pendant une rĂ©cupĂ©ration) ? :thinking:

Bons graphiques.

2 « J'aime »

en modifiant l’affichage de la date sous le graphique, on peut constater que les donnĂ©es sont bien triĂ©es. L’extraction d’une journĂ©e par Data Export sur Jeedom ramĂšne quelques rĂ©sidus sporadiques de la veille. Ce n’est pas dĂ©rangeant, vu qu’elles sont bien Ă  leur place dans la courbe.

code de la fonction modifiée pour cette confirmation :

function convertTimestampToDateTime(t) {
        const date = new Date(t * 1000); // Multiplier par 1000 pour convertir en millisecondes
        const year = date.getFullYear();
        const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Les mois commencent Ă  0
        const day = date.getDate().toString().padStart(2, '0');
        const hours = date.getHours().toString().padStart(2, '0');
        const minutes = date.getMinutes().toString().padStart(2, '0');
        const seconds = date.getSeconds().toString().padStart(2, '0');
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

C’est confirmĂ© par l’analyse du CSV gĂ©nĂ©rĂ© par le plugin Data Export.
Une extraction du 23/12 ramÚne aussi des datas du 22/12 voire 21/12 au début du fichier.

Je laisse donc les scripts et configurations en l’état, le fonctionnement est correct.
J’ai modifiĂ© le code du widget dans le tutoriel ci-dessus pour que la modale affiche date et heure complĂšte afin de faciliter l’analyse des donnĂ©es.

bonjour,
bravo pour ce tuto clair.
je suis intĂ©ressĂ© par le widget multi-graphiques mais avec la possibilitĂ© d’afficher plusieurs courbes
vous avez prĂ©vu de dĂ©velopper ça ?
j’ai constatĂ© que l’horodatage choisi au dĂ©but classique et Ă  la fin du tuto c’est timestamp , lequel choisir ?
sinon j’aimerai pouvoir codĂ© comme vous.
bonne fĂȘte de fin d’annĂ©es et Ă  vous lire.
merci

Bonjour JackDom et bienvenue sur le forum,
merci pour votre message.

Mettre plusieurs donnĂ©es sur le mĂȘme graphique exige un ajustement des Ă©chelles, plusieurs requĂȘtes jeedom, 

Mon but premier n’était pas de redĂ©velopper un grapheur avec toutes les fonctionnalitĂ©s. Jeedom le fait dĂ©jĂ . Mon but Ă©tait d’afficher l’évolution quotidienne d’une mesure, simplement sur le dashboard, sans avoir Ă  me connecter sur Jeedom.
Normalement un Data-logger Ă©tait sur la Todo-List de la V5, je ne sais pas si c’est toujours d’actualitĂ© ( @GCE ? )
Mais si la V5 a son propre data-logger, peut-ĂȘtre aura-t’elle aussi son mini grapheur. RĂȘvons un peu.

Comme expliquĂ© dans le tuto, le widget affiche HH:MM:SS afin d’optimiser la taille et le modal plein Ă©cran affiche le format complet YYYY-MM-DD HH:MM:SS afin de permettre l’analyse des donnĂ©es.

Bonne journée

OK
Merci et bonne continuation