1, 'capteurs' => 2, 'reservoirs' => 3, 'batteries' => 4); // États d'un circuit (TB_Circuits.Etat), hors 0 = OK : const CONSULTATION_ETATS_CIRCUIT = array(1 => "Limitation", 2 => "Protection", 3 => "Disjoncté", 8 => "Absence de terre", 9 => "Absence de masse"); // Types d'entrées capteur (TB_Capteurs.Type_entrees) et conversions d'unités (TB_Variables.Conversion_unite) : const CONSULTATION_TYPE_ENTREES_AUX = 1; const CONSULTATION_TYPE_ENTREES_CALCULEE_BOOLEEN = 20; const CONSULTATION_TYPE_UNITE_VOLUME = 1; const CONSULTATION_TYPE_UNITE_TEMPERATURE = 2; const CONSULTATION_TYPE_UNITE_LATITUDE = 3; const CONSULTATION_TYPE_UNITE_LONGITUDE = 4; // Historique des déclenchements d'events (Appli du CM5, bdd.h : TYPE_SOURCE_HISTORIQUE_EVENTS, EVENT_DECLENCHEMENT - // ID_Source = clé primaire de l'event). NB : le commentaire de la colonne TB_Historique.Type_source dans le schéma // ("6=usb") ne correspond pas à bdd.h - c'est bdd.h qui fait foi. const CONSULTATION_HISTORIQUE_TYPE_SOURCE_EVENTS = 6; const CONSULTATION_HISTORIQUE_EVENT_DECLENCHEMENT = 1; const CONSULTATION_HISTORIQUE_EVENT_FIN_DECLENCHEMENT = 2; // Fin de la condition (ajouté dans l'Appli en octobre 2026) // Période d'archivage par défaut (secondes) si TB_Configuration.Periode_sauvegarde_bdd est absente ou invalide : const CONSULTATION_PERIODE_ARCHIVAGE_DEFAUT = 7200; //************************************************************************************************************ // Fonction : Générations d'un bateau (base principale), de la plus récente à la plus ancienne. // Reçoit : La clé primaire du bateau // Retour : Tableau de lignes (Cle_primaire, Numero_generation, Nom_base, Motif, Version_soft, Date_debut, Date_fin) //************************************************************************************************************ function Generations_bateau($id_bateau) { $generations = array(); $reponse = Requete_preparee_bdd("SELECT Cle_primaire, Numero_generation, Nom_base, Motif, Version_soft, Date_debut, Date_fin FROM TB_Generations_bateaux WHERE ID_Bateau=? ORDER BY Numero_generation DESC", array(array('i', $id_bateau)), __FILE__, __LINE__); while ($donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC)) $generations[] = $donnees; Liberation_jeu_de_resultat_bdd($reponse); return $generations; } //************************************************************************************************************ // Fonction : Connexion DÉDIÉE à la base d'une génération (lecture), sans toucher à $_SESSION['db']. // Reçoit : Le nom de la base - TOUJOURS issu de TB_Generations_bateaux, jamais d'une saisie // Retour : L'objet mysqli, ou null en cas d'échec (journalisé) //************************************************************************************************************ function Connexion_generation($nom_base) { if (preg_match('/^[A-Za-z0-9_]+$/', (string)$nom_base) !== 1) return null; // Serveur : celui de la base principale (BDD_HOST), joignable depuis le conteneur du site - les bases des // générations sont sur le même serveur MariaDB. BDD_BATEAUX_HOST/PORT (127.0.0.1:3307) ne conviennent qu'aux // scripts NATIFS du VPS (archivage) : dans le conteneur, 127.0.0.1 désigne le conteneur lui-même (synthèse §50). // Compte : celui des bases des bateaux (à remplacer par un compte en lecture seule - lot transverse). mysqli_report(MYSQLI_REPORT_OFF); $hote = BDD_HOST; $port = null; if (preg_match('/^(.+):(\d+)$/', $hote, $m) === 1) { $hote = $m[1]; $port = (int)$m[2]; } $db = @new mysqli($hote, BDD_BATEAUX_USER, BDD_BATEAUX_PASSWORD, $nom_base, $port); if ($db->connect_errno) { Enregistrement_log("\tConnexion_generation : échec de connexion à ".$nom_base." (".$db->connect_error.")", __FILE__, __LINE__); return null; } $db->set_charset('utf8mb4'); // Dates des colonnes TIMESTAMP (TB_Historique...) lues en UTC, comme elles sont produites par le CM5, quel que soit // le fuseau par défaut du serveur MariaDB : $db->query("SET time_zone = '+00:00'"); return $db; } //************************************************************************************************************ // Fonction : Valeur d'un paramètre de TB_Configuration de la base du bateau (telle qu'à la dernière archive). // Retour : La valeur (chaîne), ou $defaut si absente //************************************************************************************************************ function Configuration_bateau($db, $nom, $defaut = null) { $stmt = $db->prepare("SELECT Valeur FROM TB_Configuration WHERE Nom=?"); if (!$stmt) return $defaut; $stmt->bind_param('s', $nom); $stmt->execute(); $ligne = $stmt->get_result()->fetch_row(); $stmt->close(); return $ligne ? $ligne[0] : $defaut; } //************************************************************************************************************ // Fonction : Période d'archivage du bateau, en secondes (TB_Configuration.Periode_sauvegarde_bdd) - base des // seuils de l'indicateur de fraîcheur (orange jusqu'à 1,25 fois la période, rouge au-delà). //************************************************************************************************************ function Periode_archivage_bateau($db) { $periode = (int)Configuration_bateau($db, 'Periode_sauvegarde_bdd', 0); return ($periode > 0) ? $periode : CONSULTATION_PERIODE_ARCHIVAGE_DEFAUT; } //************************************************************************************************************ // Conversions et formats - traduction de l'API du CM5 (conversion_volume, conversion_temperature, // symbole_unite_volume, symbole_unite_temperature, chaine_latitude, chaine_longitude). //************************************************************************************************************ function Conversion_volume($valeur, $unite) { switch ((int)$unite) { case 1: return $valeur / 3.785411784; case 2: return $valeur / 4.546090; default: return $valeur; } } function Symbole_unite_volume($unite) { switch ((int)$unite) { case 1: return "US Ga"; case 2: return "Imp. Ga"; default: return "lit."; } } function Conversion_temperature($valeur, $unite) { return ((int)$unite == 1) ? ($valeur * 1.8) + 32.0 : $valeur; } function Symbole_unite_temperature($unite) { return ((int)$unite == 1) ? "°F" : "°C"; } function Chaine_coordonnee($valeur, $positif, $negatif) { // Le signe est porté par la lettre (N/S, E/W) : degrés et minutes en valeur absolue. (L'API du CM5 garde le signe, // ce qui donne par exemple -12°-30.000' S pour -12,5 - à corriger aussi de son côté.) $absolue = abs((float)$valeur); $degres = (int)$absolue; $minutes = ($absolue - $degres) * 60.0; return $degres."°".number_format($minutes, 3, '.', '')."' ".(($valeur >= 0) ? $positif : $negatif); } function Nombre_affiche($valeur, $precision) { return number_format((float)$valeur, max(0, (int)$precision), ',', ' '); } //************************************************************************************************************ // Fonction : Éléments d'un domaine, regroupés par famille, avec leurs dernières valeurs (telles qu'à la dernière // archive), prêts à afficher. Mêmes éléments que l'écran du bord : circuits et capteurs dont // Priorite_affichage_accueil > 0, toutes les batteries et tous les réservoirs. // Reçoit : La connexion à la base de génération, le domaine ('circuits', 'capteurs', 'batteries', 'reservoirs') // Retour : Tableau de familles : ['nom' => ..., 'elements' => [['nom'=>..., 'valeurs'=>[...], 'defaut'=>texte|null]]] // Les textes sont BRUTS (à échapper à l'affichage). //************************************************************************************************************ function Elements_domaine($db, $domaine) { if (!isset(CONSULTATION_TYPE_SOURCE[$domaine])) return array(); $familles = array(); $stmt = $db->prepare("SELECT Cle_primaire, Nom FROM TB_Familles WHERE Type_source=? ORDER BY Priorite_affichage, Cle_primaire"); $type_source = CONSULTATION_TYPE_SOURCE[$domaine]; $stmt->bind_param('i', $type_source); $stmt->execute(); $resultat = $stmt->get_result(); while ($famille = $resultat->fetch_assoc()) $familles[(int)$famille['Cle_primaire']] = array('nom' => $famille['Nom'], 'elements' => array()); $stmt->close(); if (count($familles) == 0) return array(); switch ($domaine) { case 'circuits': $requete = "SELECT Cle_primaire, ID_Famille, Nom, Etat_systeme, Etat, Consigne, Tension, Intensite FROM TB_Circuits WHERE Priorite_affichage_accueil > 0 ORDER BY Priorite_affichage_accueil, Cle_primaire"; break; case 'capteurs': $requete = "SELECT C.Cle_primaire, C.ID_Famille, C.Nom, C.Etat_systeme, C.Etat, C.Type_entrees, C.Valeur, C.Precision_affichage_accueil, C.Liste_valeurs_textes, V.Unite, V.Conversion_unite FROM TB_Capteurs C LEFT JOIN TB_Variables V ON V.Cle_primaire = C.ID_Variable WHERE C.Priorite_affichage_accueil > 0 ORDER BY C.Priorite_affichage_accueil, C.Nom"; $unite_volume = Configuration_bateau($db, 'Unite_volume', 0); $unite_temperature = Configuration_bateau($db, 'Unite_temperature', 0); break; case 'batteries': $requete = "SELECT Cle_primaire, ID_Famille, Nom, Etat_systeme, Soc, Tension FROM TB_Batteries ORDER BY Priorite_affichage_accueil, Cle_primaire"; break; case 'reservoirs': $requete = "SELECT Cle_primaire, ID_Famille, Nom, Etat_systeme, Etat_entrees, Niveau, Capacite FROM TB_Reservoirs ORDER BY Priorite_affichage_accueil, Cle_primaire"; $unite_volume = Configuration_bateau($db, 'Unite_volume', 0); break; } $resultat = $db->query($requete); if (!$resultat) return array(); while ($e = $resultat->fetch_assoc()) { $id_famille = (int)$e['ID_Famille']; if (!isset($familles[$id_famille])) continue; // Élément rattaché à une famille d'un autre domaine ou inexistante $valeurs = array(); $defaut = ((int)$e['Etat_systeme'] != 0) ? t("Défaut système") : null; $courbe = true; // Courbe historique disponible (pas pour une latitude ou une longitude) switch ($domaine) { case 'circuits': // Consigne en % (100 = marche pleine ; une valeur intermédiaire = variateur) : $consigne = (int)$e['Consigne']; if ($consigne <= 0) $valeurs[] = t("Arrêt"); else if ($consigne >= 100) $valeurs[] = t("Marche"); else $valeurs[] = t("Marche")." (".$consigne." %)"; if ((float)$e['Intensite'] != 0) $valeurs[] = Nombre_affiche($e['Intensite'], 1)." A"; if ((float)$e['Tension'] != 0) $valeurs[] = Nombre_affiche($e['Tension'], 1)." V"; if ($defaut === null && (int)$e['Etat'] != 0) $defaut = t(CONSULTATION_ETATS_CIRCUIT[(int)$e['Etat']] ?? "Défaut"); break; case 'capteurs': $brute = (float)$e['Valeur']; $type_unite = (int)$e['Conversion_unite']; $texte_liste = null; if ((int)$e['Type_entrees'] == CONSULTATION_TYPE_ENTREES_AUX || (int)$e['Type_entrees'] == CONSULTATION_TYPE_ENTREES_CALCULEE_BOOLEEN) { foreach (explode(';', (string)$e['Liste_valeurs_textes']) as $paire) { $champs = explode('/', $paire); if (count($champs) == 2 && (float)$champs[0] == $brute) { $texte_liste = $champs[1]; break; } } } if ($type_unite == CONSULTATION_TYPE_UNITE_LATITUDE) { $valeurs[] = Chaine_coordonnee($brute, "N", "S"); $courbe = false; } else if ($type_unite == CONSULTATION_TYPE_UNITE_LONGITUDE) { $valeurs[] = Chaine_coordonnee($brute, "E", "W"); $courbe = false; } else if ($texte_liste !== null) $valeurs[] = $texte_liste; else if ($type_unite == CONSULTATION_TYPE_UNITE_VOLUME) $valeurs[] = Nombre_affiche(Conversion_volume($brute, $unite_volume), $e['Precision_affichage_accueil'])." ".Symbole_unite_volume($unite_volume); else if ($type_unite == CONSULTATION_TYPE_UNITE_TEMPERATURE) $valeurs[] = Nombre_affiche(Conversion_temperature($brute, $unite_temperature), $e['Precision_affichage_accueil'])." ".Symbole_unite_temperature($unite_temperature); else $valeurs[] = trim(Nombre_affiche($brute, $e['Precision_affichage_accueil'])." ".$e['Unite']); if ($defaut === null && (int)$e['Etat'] > 0) $defaut = t("Défaut capteur"); break; case 'batteries': $valeurs[] = Nombre_affiche($e['Soc'], 0)." %"; $valeurs[] = Nombre_affiche($e['Tension'], 2)." V"; break; case 'reservoirs': $valeurs[] = Nombre_affiche($e['Niveau'], 0)." %"; if ((float)$e['Capacite'] > 0) { $valeurs[] = "≈ ".Nombre_affiche(Conversion_volume((float)$e['Niveau'] * (float)$e['Capacite'] / 100.0, $unite_volume), 0)." ".Symbole_unite_volume($unite_volume); } if ($defaut === null && (int)$e['Etat_entrees'] > 0) $defaut = t("Défaut sondes"); break; } $familles[$id_famille]['elements'][] = array('id' => (int)$e['Cle_primaire'], 'nom' => $e['Nom'], 'valeurs' => $valeurs, 'defaut' => $defaut, 'courbe' => $courbe); } $resultat->free(); // Familles sans élément affiché : retirées return array_values(array_filter($familles, fn($f) => count($f['elements']) > 0)); } //************************************************************************************************************ // Fonction : Derniers ÉPISODES d'alarmes (synthèse §50), à partir des déclenchements (EVENT_DECLENCHEMENT) et des fins // de condition (EVENT_FIN_DECLENCHEMENT) enregistrés dans TB_Historique. // - Alarme = event ayant au moins un indicateur Alarme_... actif (son, LED, soutien, source, message, SMS, // e-mail), selon sa définition ACTUELLE - un event sans indicateur (bouton poussoir...) n'apparaît jamais. // - Event à condition sur une source (Type_source 1 à 4) : un épisode = un déclenchement et la fin // suivante du même event, avec sa durée. Cas particuliers signalés : déclenchement le plus récent sans fin // ('en_cours' : en cours à la dernière archive), déclenchement suivi d'un autre déclenchement sans fin // entre les deux ('fin_non_enregistree' : données antérieures à l'enregistrement des fins), fin dont le // déclenchement est hors de la période lue ('debut_anterieur'). // - Event horaire (Type_source 0) : ponctuel, sans durée ni fin ('ponctuel'). // Reçoit : La connexion à la base de génération, le nombre maximal d'épisodes // Retour : Tableau, du plus récent au plus ancien, de ['debut_utc', 'fin_utc' (ISO 8601 Z ou null), 'duree' (secondes // ou null), 'statut' (termine, en_cours, fin_non_enregistree, debut_anterieur, ponctuel), 'nom', // 'message'] - textes bruts, à échapper à l'affichage //************************************************************************************************************ function Episodes_alarmes($db, $limite = 20) { // Les dernières lignes (déclenchements et fins), lues en ordre décroissant puis traitées dans l'ordre chronologique. // Marge de lecture : plusieurs lignes par épisode, et des events qui s'entremêlent. $stmt = $db->prepare("SELECT H.Date, H.Type, H.ID_Source, E.Nom, E.Texte_message, E.Type_source FROM TB_Historique H JOIN TB_Events E ON E.Cle_primaire = H.ID_Source WHERE H.Type_source = ? AND H.Type IN (?, ?) AND (E.Alarme_son > 0 OR E.Alarme_led > 0 OR E.Alarme_soutien > 0 OR E.Alarme_source > 0 OR E.Alarme_message > 0 OR E.Alarme_sms > 0 OR E.Alarme_email > 0) ORDER BY H.Date DESC LIMIT ?"); if (!$stmt) return array(); $type_source = CONSULTATION_HISTORIQUE_TYPE_SOURCE_EVENTS; $debut = CONSULTATION_HISTORIQUE_EVENT_DECLENCHEMENT; $fin = CONSULTATION_HISTORIQUE_EVENT_FIN_DECLENCHEMENT; $nb_lignes = $limite * 10; $stmt->bind_param('iiii', $type_source, $debut, $fin, $nb_lignes); $stmt->execute(); $lignes = array_reverse($stmt->get_result()->fetch_all(MYSQLI_ASSOC)); $stmt->close(); $iso = function($date) { return str_replace(' ', 'T', $date).'Z'; }; $episodes = array(); $ouverts = array(); // id_event => index de l'épisode ouvert (déclenchement sans fin pour l'instant) foreach ($lignes as $l) { $id = (int)$l['ID_Source']; $base = array('nom' => $l['Nom'], 'message' => trim((string)$l['Texte_message'])); if ((int)$l['Type_source'] == 0) // Event horaire : ponctuel { if ((int)$l['Type'] == $debut) $episodes[] = $base + array('debut_utc' => $iso($l['Date']), 'fin_utc' => null, 'duree' => null, 'statut' => 'ponctuel', 'tri' => $l['Date']); continue; } if ((int)$l['Type'] == $debut) { if (isset($ouverts[$id])) $episodes[$ouverts[$id]]['statut'] = 'fin_non_enregistree'; $episodes[] = $base + array('debut_utc' => $iso($l['Date']), 'fin_utc' => null, 'duree' => null, 'statut' => 'en_cours', 'tri' => $l['Date']); $ouverts[$id] = count($episodes) - 1; } else { if (isset($ouverts[$id])) { $i = $ouverts[$id]; $episodes[$i]['fin_utc'] = $iso($l['Date']); $episodes[$i]['duree'] = max(0, strtotime($l['Date'].' UTC') - strtotime(str_replace(array('T', 'Z'), array(' ', ''), $episodes[$i]['debut_utc']).' UTC')); $episodes[$i]['statut'] = 'termine'; $episodes[$i]['tri'] = $l['Date']; unset($ouverts[$id]); } else $episodes[] = $base + array('debut_utc' => null, 'fin_utc' => $iso($l['Date']), 'duree' => null, 'statut' => 'debut_anterieur', 'tri' => $l['Date']); } } // Du plus récent au plus ancien (date de la dernière ligne connue de l'épisode), limité : usort($episodes, fn($a, $b) => strcmp($b['tri'], $a['tri'])); return array_slice($episodes, 0, $limite); } //************************************************************************************************************ // Fonction : Durée lisible ("45 s", "27 min", "2 h 05 min", "3 j 4 h"). //************************************************************************************************************ function Duree_lisible($secondes) { $secondes = (int)$secondes; if ($secondes < 60) return $secondes." s"; if ($secondes < 3600) return intdiv($secondes, 60)." min"; if ($secondes < 86400) return intdiv($secondes, 3600)." h ".sprintf("%02d", intdiv($secondes % 3600, 60))." min"; return intdiv($secondes, 86400)." j ".intdiv($secondes % 86400, 3600)." h"; } //************************************************************************************************************ // COURBES HISTORIQUES (synthèse §50, étape 3c) - logique reprise de la route /mesures de l'API du CM5 (variable // selon le domaine, conversions d'unités, dernière valeur connue reportée au début de la période), avec trois // différences voulues : // - la courbe s'arrête à la DERNIÈRE ARCHIVE (ou à la fin de la génération), pas à "maintenant" : au-delà, il n'y a // pas de données, et prolonger la dernière valeur en inventerait ; // - périodes étendues (24 h à 1 an), le VPS conservant tout l'historique ; // - regroupement par tranche avec MOYENNE, MINIMUM et MAXIMUM : la courbe moyenne est entourée d'une bande min-max, // pour qu'un pic bref (30 V pendant quelques secondes sur une tension de 26 V) reste visible même sur une longue // période - une simple moyenne par tranche l'effaçait. Les statistiques min/max/moyenne sont calculées sur les // mesures BRUTES de la période (et non sur les moyennes par tranche, qui les sous-estimaient). //************************************************************************************************************ const CONSULTATION_PERIODES_COURBE = array('24h' => 86400, '7j' => 604800, '30j' => 2592000, '3m' => 7776000, '1a' => 31536000); const CONSULTATION_POINTS_CIBLE = 300; // Nombre de tranches visé, quelle que soit la période // Variables traçables pour les circuits et les batteries (le client choisit) : code TB_Variables, colonne de la table // permanente (valeur actuelle), précision. Capteurs et réservoirs : variable unique. const CONSULTATION_VARIABLES_CIRCUIT = array( 'consigne' => array('code' => 'Circ_consigne', 'precision' => 0), 'intensite' => array('code' => 'Circ_courant', 'precision' => 2), // 1 décimale sur SP4AC 'tension' => array('code' => 'Circ_tension', 'precision' => 2), // Circ_tension_secteur et 0 décimale sur SP4AC 'frequence' => array('code' => 'Circ_frequence', 'precision' => 1)); const CONSULTATION_VARIABLES_BATTERIE = array( 'soc' => array('code' => 'Batt_soc', 'precision' => 1), 'tension' => array('code' => 'Batt_tension', 'precision' => 2), 'temperature' => array('code' => 'Batt_temperature', 'precision' => 1), 'courant_differentiel' => array('code' => 'Batt_courant_differentiel', 'precision' => 2), 'courant_charge' => array('code' => 'Batt_courant_charge', 'precision' => 2), 'courant_decharge' => array('code' => 'Batt_courant_decharge', 'precision' => 2), 'soh' => array('code' => 'Batt_soh', 'precision' => 1), 'autonomie' => array('code' => 'Batt_autonomie', 'precision' => 1)); const CONSULTATION_TYPE_MODULE_SP4AC = 3; //************************************************************************************************************ // Fonction : Données d'une courbe historique. // Reçoit : La connexion à la base de génération, le domaine, l'élément (clé primaire), la variable (circuits, // batteries ; ignorée sinon), la période (clé de CONSULTATION_PERIODES_COURBE), la date de fin (UTC, // 'Y-m-d H:i:s' : dernière archive, ou fin de la génération) // Retour : ['debut', 'fin' (ms), 'unite', 'precision', 'points' => [['t' (ms), 'v', 'min', 'max']], 'stats' (ou null) // => ['min' => ['v', 't'], 'max' => ['v', 't'], 'moyenne']] ou ['erreur' => texte] //************************************************************************************************************ function Courbe_element($db, $type, $id_element, $variable, $periode, $date_fin_utc) { if (!isset(CONSULTATION_PERIODES_COURBE[$periode])) return array('erreur' => "période invalide"); $fin = strtotime($date_fin_utc.' UTC'); if ($fin === false) return array('erreur' => "date de fin invalide"); $duree = CONSULTATION_PERIODES_COURBE[$periode]; $debut = $fin - $duree; $regroupement = max(1, intdiv($duree, CONSULTATION_POINTS_CIBLE)); $valeur_unique = function($requete, $types, ...$parametres) use ($db) { $stmt = $db->prepare($requete); if (!$stmt) return null; $stmt->bind_param($types, ...$parametres); $stmt->execute(); $ligne = $stmt->get_result()->fetch_row(); $stmt->close(); return $ligne; }; $id_variable_code = function($code) use ($valeur_unique) { $ligne = $valeur_unique("SELECT Cle_primaire FROM TB_Variables WHERE Code=?", 's', $code); return $ligne ? (int)$ligne[0] : 0; }; // Variable et table selon le domaine : switch ($type) { case 'capteurs': $table = "TB_Mesures_capteurs"; $ligne = $valeur_unique("SELECT ID_Variable, Precision_affichage_mesures FROM TB_Capteurs WHERE Cle_primaire=?", 'i', $id_element); if (!$ligne) return array('erreur' => "capteur introuvable"); $id_variable = (int)$ligne[0]; $precision = (int)$ligne[1]; break; case 'reservoirs': $table = "TB_Mesures_reservoirs"; if (!$valeur_unique("SELECT 1 FROM TB_Reservoirs WHERE Cle_primaire=?", 'i', $id_element)) return array('erreur' => "réservoir introuvable"); $id_variable = $id_variable_code('Res_niveau'); $precision = 1; break; case 'circuits': $table = "TB_Mesures_circuits"; if (!isset(CONSULTATION_VARIABLES_CIRCUIT[$variable])) return array('erreur' => "variable invalide"); $ligne = $valeur_unique("SELECT (SELECT Type FROM TB_Modules_config WHERE Adresse_k_bus=TB_Circuits.Adresse_module) FROM TB_Circuits WHERE Cle_primaire=?", 'i', $id_element); if (!$ligne) return array('erreur' => "circuit introuvable"); $sp4ac = ((int)$ligne[0] == CONSULTATION_TYPE_MODULE_SP4AC); $code = CONSULTATION_VARIABLES_CIRCUIT[$variable]['code']; $precision = CONSULTATION_VARIABLES_CIRCUIT[$variable]['precision']; if ($sp4ac && $variable == 'tension') { $code = 'Circ_tension_secteur'; $precision = 0; } if ($sp4ac && $variable == 'intensite') $precision = 1; $id_variable = $id_variable_code($code); break; case 'batteries': $table = "TB_Mesures_batteries"; if (!isset(CONSULTATION_VARIABLES_BATTERIE[$variable])) return array('erreur' => "variable invalide"); if (!$valeur_unique("SELECT 1 FROM TB_Batteries WHERE Cle_primaire=?", 'i', $id_element)) return array('erreur' => "batterie introuvable"); $id_variable = $id_variable_code(CONSULTATION_VARIABLES_BATTERIE[$variable]['code']); $precision = CONSULTATION_VARIABLES_BATTERIE[$variable]['precision']; break; default: return array('erreur' => "domaine invalide"); } if ($id_variable == 0) return array('erreur' => "variable introuvable dans la configuration du bateau"); // Unité et conversion (unités du bateau) : $ligne = $valeur_unique("SELECT Unite, Conversion_unite FROM TB_Variables WHERE Cle_primaire=?", 'i', $id_variable); $unite = $ligne ? (string)$ligne[0] : ""; $type_unite = $ligne ? (int)$ligne[1] : 0; if ($type_unite == CONSULTATION_TYPE_UNITE_LATITUDE || $type_unite == CONSULTATION_TYPE_UNITE_LONGITUDE) return array('erreur' => "pas de courbe pour une coordonnée"); $unite_volume = (int)Configuration_bateau($db, 'Unite_volume', 0); $unite_temperature = (int)Configuration_bateau($db, 'Unite_temperature', 0); if ($type_unite == CONSULTATION_TYPE_UNITE_VOLUME) $unite = Symbole_unite_volume($unite_volume); else if ($type_unite == CONSULTATION_TYPE_UNITE_TEMPERATURE) $unite = Symbole_unite_temperature($unite_temperature); $convertir = function($brut) use ($type_unite, $unite_volume, $unite_temperature) { if ($brut === null) return null; if ($type_unite == CONSULTATION_TYPE_UNITE_VOLUME) return Conversion_volume((float)$brut, $unite_volume); if ($type_unite == CONSULTATION_TYPE_UNITE_TEMPERATURE) return Conversion_temperature((float)$brut, $unite_temperature); return (float)$brut; }; $date_debut = gmdate('Y-m-d H:i:s', $debut); $date_fin = gmdate('Y-m-d H:i:s', $fin); $points = array(); $stats_min = null; $stats_max = null; // Dernière valeur connue AVANT la période, reportée au premier instant (élément enregistré seulement sur changement) : $ligne = $valeur_unique("SELECT Valeur FROM ".$table." WHERE ID=? AND ID_Variable=? AND Date<=? AND Valeur IS NOT NULL ORDER BY Date DESC LIMIT 1", 'iis', $id_element, $id_variable, $date_debut); if ($ligne) { $v = $convertir($ligne[0]); $points[] = array('t' => $debut * 1000, 'v' => $v, 'min' => $v, 'max' => $v); $stats_min = array('v' => $v, 't' => $debut * 1000); $stats_max = array('v' => $v, 't' => $debut * 1000); } // Tranches : moyenne, minimum, maximum. $stmt = $db->prepare("SELECT UNIX_TIMESTAMP(MIN(Date)), AVG(Valeur), MIN(Valeur), MAX(Valeur) FROM ".$table." WHERE ID=? AND ID_Variable=? AND Date>? AND Date<=? AND Valeur IS NOT NULL GROUP BY UNIX_TIMESTAMP(Date) DIV ? ORDER BY 1"); $stmt->bind_param('iissi', $id_element, $id_variable, $date_debut, $date_fin, $regroupement); $stmt->execute(); $resultat = $stmt->get_result(); while ($l = $resultat->fetch_row()) { $points[] = array('t' => (int)$l[0] * 1000, 'v' => $convertir($l[1]), 'min' => $convertir($l[2]), 'max' => $convertir($l[3])); } $stmt->close(); // Statistiques sur les mesures BRUTES de la période (avec la valeur reportée au début) : $ligne = $valeur_unique("SELECT AVG(Valeur), COUNT(*) FROM ".$table." WHERE ID=? AND ID_Variable=? AND Date>? AND Date<=? AND Valeur IS NOT NULL", 'iiss', $id_element, $id_variable, $date_debut, $date_fin); $moyenne = ($ligne && (int)$ligne[1] > 0) ? $convertir($ligne[0]) : (($stats_min !== null) ? $stats_min['v'] : null); if ($ligne && (int)$ligne[1] > 0) { foreach (array('ASC' => 'min', 'DESC' => 'max') as $ordre => $cle) { $extreme = $valeur_unique("SELECT Valeur, UNIX_TIMESTAMP(Date) FROM ".$table." WHERE ID=? AND ID_Variable=? AND Date>? AND Date<=? AND Valeur IS NOT NULL ORDER BY Valeur ".$ordre.", Date DESC LIMIT 1", 'iiss', $id_element, $id_variable, $date_debut, $date_fin); $candidat = array('v' => $convertir($extreme[0]), 't' => (int)$extreme[1] * 1000); if ($cle == 'min' && ($stats_min === null || $candidat['v'] < $stats_min['v'])) $stats_min = $candidat; if ($cle == 'max' && ($stats_max === null || $candidat['v'] > $stats_max['v'])) $stats_max = $candidat; } } // Point de fin : dernière valeur MESURÉE (et non la moyenne de la dernière tranche) prolongée jusqu'à la dernière // archive, pas au-delà : if (count($points) > 0 && end($points)['t'] < $fin * 1000) { $ligne = $valeur_unique("SELECT Valeur FROM ".$table." WHERE ID=? AND ID_Variable=? AND Date<=? AND Valeur IS NOT NULL ORDER BY Date DESC LIMIT 1", 'iis', $id_element, $id_variable, $date_fin); $v = $ligne ? $convertir($ligne[0]) : end($points)['v']; $points[] = array('t' => $fin * 1000, 'v' => $v, 'min' => $v, 'max' => $v); } return array('debut' => $debut * 1000, 'fin' => $fin * 1000, 'unite' => $unite, 'precision' => $precision, 'points' => $points, 'stats' => ($stats_min !== null) ? array('min' => $stats_min, 'max' => $stats_max, 'moyenne' => $moyenne) : null); } //************************************************************************************************************ // DEMANDE D'ARCHIVE À L'OUVERTURE DE LA PAGE (synthèse §50, étape 3d). // Le portail (conteneur) ne peut pas lancer le traitement des archives (natif : client mysql, openssl, dossiers // d'archives) : il DÉPOSE une demande - un fichier nommé d'après le numéro du bateau dans /var/www/demandes/a_traiter - // que l'unité systemd ship2net-demandes.path fait traiter aussitôt (traiter_demandes_archivage.php : archive immédiate // demandée au CM5, puis rapatriement et chargement). // Garde-fous : pas de demande si les données ont moins de Delai_demande_archive secondes (TB_Configuration, 300 par // défaut), ni si une demande pour ce bateau date de moins de ce même délai (marqueur /var/www/demandes/dernieres/). //************************************************************************************************************ const REPERTOIRE_DEMANDES = "/var/www/demandes"; const DELAI_DEMANDE_ARCHIVE_DEFAUT = 300; // secondes function Delai_demande_archive() { $reponse = Requete_preparee_bdd("SELECT Valeur FROM TB_Configuration WHERE Nom=? AND Validite=1", array(array('s', 'Delai_demande_archive')), __FILE__, __LINE__); $donnees = Recherche_ligne_bdd($reponse, MYSQLI_ASSOC); Liberation_jeu_de_resultat_bdd($reponse); $delai = $donnees ? (int)$donnees['Valeur'] : 0; return ($delai > 0) ? $delai : DELAI_DEMANDE_ARCHIVE_DEFAUT; } // Retour : ['statut' => 'demandee' | 'donnees_recentes' | 'deja_demandee' | 'impossible', 'attente' => secondes avant // qu'une nouvelle demande soit possible (0 si sans objet)] - la page l'affiche, pour que l'utilisateur sache // pourquoi aucune mise à jour n'est demandée. function Demander_archive_si_besoin($id_bateau, $date_derniere_maj_utc) { $id_bateau = (int)$id_bateau; if ($id_bateau <= 0) return array('statut' => 'impossible', 'attente' => 0); $delai = Delai_demande_archive(); // Données déjà récentes : if ($date_derniere_maj_utc !== null && $date_derniere_maj_utc >= "2020-01-02") { $age = time() - strtotime($date_derniere_maj_utc.' UTC'); if ($age < $delai) return array('statut' => 'donnees_recentes', 'attente' => $delai - $age); } // Demande récente pour ce bateau (quel que soit le nombre de pages ouvertes ou de rechargements) : $marqueur = REPERTOIRE_DEMANDES."/dernieres/".$id_bateau; if (is_file($marqueur)) { $age = time() - filemtime($marqueur); if ($age < $delai) return array('statut' => 'deja_demandee', 'attente' => $delai - $age); } if (!@touch($marqueur) || @file_put_contents(REPERTOIRE_DEMANDES."/a_traiter/".$id_bateau, gmdate('Y-m-d H:i:s')." UTC\n") === false) { Enregistrement_log("\tDemander_archive_si_besoin : dépôt impossible dans ".REPERTOIRE_DEMANDES." (bateau ".$id_bateau.")", __FILE__, __LINE__); return array('statut' => 'impossible', 'attente' => 0); } return array('statut' => 'demandee', 'attente' => 0); }