Mise à jour des fichiers CSS, ajout des mécanismes de mise à jour de Ship2net avec info sur la page du bateau.

This commit is contained in:
2026-10-06 16:25:29 +00:00
parent ce81084cdc
commit 0a2ec261ec
24 changed files with 2682 additions and 2263 deletions
+1
View File
@@ -59,6 +59,7 @@ define("ANNEE_CREATION", 2019);
//=====================================================================
// Répertoires et fichiers :
define("REPERTOIRE_TEMP", REPERTOIRE_HOME."/temp"); // Pas de '/' de terminaison
define("REPERTOIRE_LOGS", REPERTOIRE_HOME."/logs"); // Pas de '/' de terminaison
if (PRODUCTION)
{
+1
View File
@@ -9,5 +9,6 @@ require_once(__DIR__.'/fonctions_mail.php'); // Contient les fonctions d'émi
require_once(__DIR__.'/fonctions_pdf.php'); // Contient les fonctions de création de fichiers PDF
require_once(__DIR__.'/fonctions_traductions.php');
require_once(__DIR__.'/fonctions_utilitaires.php');
require_once(__DIR__.'/fonctions_consultation.php'); // Consultation des données d'un bateau (bases de génération)
+1 -1
View File
@@ -119,7 +119,7 @@ function Ouverture_bdd_bateau($id_bateau)
return false;
}
$requete = "SELECT Nom_base FROM TB_Generations_bateau WHERE ID_Bateau=? AND Date_fin IS NULL";
$requete = "SELECT Nom_base FROM TB_Generations_bateaux WHERE ID_Bateau=? AND Date_fin IS NULL";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('i', $id_bateau);
$stmt->execute();
+584
View File
@@ -0,0 +1,584 @@
<?php
//************************************************************************************************************
// fonctions_consultation.php - Consultation des données d'un bateau sur le portail, à partir de la base de
// génération alimentée par les archives (lot 3, synthèse §50).
//
// Les règles d'affichage (familles, éléments affichés, conversions d'unités, valeurs textuelles, coordonnées) sont
// reprises de l'API du CM5 (route /elements de shipheart_api, elle-même fidèle à l'application native), pour que
// le portail montre les valeurs comme l'écran du bord.
//
// Ces fonctions n'utilisent JAMAIS $_SESSION['db'] pour la base du bateau : une connexion dédiée (mysqli) est
// ouverte et refermée par la page, la connexion principale du site reste intacte.
//************************************************************************************************************
// Types de sources (TB_Familles.Type_source) :
const CONSULTATION_TYPE_SOURCE = array('circuits' => 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/<N>).
//************************************************************************************************************
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);
}
+17 -74
View File
@@ -108,12 +108,11 @@ function Entete_page_html()
echo '<link rel="icon" type="image/png" href="images/favicon.png" />';
echo "<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>";
echo "<link href='https://fonts.googleapis.com/css2?family=Oswald&display=swap' rel='stylesheet'>";
echo "<link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' integrity='sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=' crossorigin=''/>";
echo "<link rel='stylesheet' href='/vendor/noUiSlider/dist/nouislider.min.css'>";
echo "<script src='/vendor/noUiSlider/dist/nouislider.min.js'></script>";
// NB (synthèse §50) : les polices Montserrat et Oswald ne sont plus chargées depuis fonts.googleapis.com - aucune
// feuille de style ne les utilisait (police du système), et chaque visite transmettait l'adresse IP du visiteur à
// Google (problème RGPD).
// NB (synthèse §50) : Leaflet (cartographie, chargé depuis unpkg.com) et noUiSlider (curseur de dates) ne sont plus
// chargés - ils servaient aux onglets du modèle CM4, retirés au §47 ; aucune page ne les utilise plus.
// gestion des fichiers CSS (et de scripts spécifiques à certains fichiers) :
switch(basename($_SERVER['SCRIPT_FILENAME']))
@@ -130,10 +129,13 @@ function Entete_page_html()
echo '<link rel="stylesheet" type="text/css" href="/vendor/bootstrap/bootstrap.min.css" />'; // CSS de Bootstrap
echo '<link rel="stylesheet" type="text/css" media="screen" href="format.css" />'; // CSS général, tous écrans.
echo '<link rel="stylesheet" type="text/css" media="screen and (max-device-width : 667px)" href="format__telephone.css" />'; // CSS pour les petits écrans (doit être placé à la fin car il override les CSS globaux)
// NB (synthèse §50) : "max-width" (largeur de la FENÊTRE) au lieu de "max-device-width" (taille physique de l'écran,
// condition dépréciée) - même effet sur un téléphone, et cohérent avec les règles "petit écran" de format.css.
echo '<link rel="stylesheet" type="text/css" media="screen and (max-width: 667px)" href="format__telephone.css" />'; // CSS pour les petits écrans (doit être placé à la fin car il override les CSS globaux)
// Chargement de srcipts JS généraux :
echo '<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>'; // Pour Google Charts (graphiques)
// NB (synthèse §50) : Google Charts n'est plus chargé (depuis les serveurs de Google, sur toutes les pages) - les
// graphiques utilisent Chart.js, hébergé sur le VPS (vendor/chartjs) et chargé seulement par la page qui en a besoin.
echo '<script src="/vendor/jQuery/jquery-3.6.0.min.js"></script>';
@@ -167,11 +169,9 @@ function Entete_page($message_erreur, $message_information)
//----------------------------------
// Bandeau gauche :
echo '<div id="logo_home">
<a id="home" href="">
<a href="shipmembers.php" style="padding: 0;" id="logo_heart_a" title="'.t("Retour à l'accueil").'">
<div id="logo_heart"> </div>
<div id="logo_texte"> </div>
</a>
<a href="shipmembers.php" style="padding: 0;" id="logo_heart_a" title="'.t("Retour à l'accueil").'">
<div id="logo_heart"> </div>
<div id="logo_texte"> </div>
</a>
</div>';
@@ -209,68 +209,11 @@ function Entete_page($message_erreur, $message_information)
echo '</div>';
//--------------------------------------------------------------------------------------------
// Menu afficher / masquer -- Mobile
//--------------------------------------------------------------------------------------------
echo '<script>
window.onload = function () {
pro = document.getElementsByClassName("portail_pro");
par = document.getElementsByClassName("portail_par");
if ($(window).width() > 850) {
if (pro.length > 0) { for (i = 0; i < pro.length ; i++){ pro[i].hidden=true; } document.getElementById(\'par_p\').hidden = true }
if (par.length > 0) { for (i = 0; i < par.length ; i++){ par[i].hidden=true; } document.getElementById(\'pro_p\').hidden = true }
document.getElementById(\'img_type\').hidden= false;
document.getElementById(\'logo_heart_a\').href = "bienvenue.php";
} else {
if (pro.length > 0) { for (i = 0; i < pro.length ; i++){ pro[i].hidden=false; } document.getElementById(\'par_p\').hidden = false }
if (par.length > 0) { for (i = 0; i < par.length ; i++){ par[i].hidden=false; } document.getElementById(\'pro_p\').hidden = false }
document.getElementById(\'img_type\').hidden= true;
document.getElementById(\'logo_heart_a\').href = "accueil.php";
}
} ;
$(document).ready(function(){
function menu_control(){
hidden_div = document.getElementById("nav_bar_global").style.display;
hidden_div.replace(/[\u200B-\u200D\uFEFF]/g, \'\');
if (hidden_div == "block") hidden_div = "none";
else if (hidden_div == "none") hidden_div = "block";
document.getElementById("nav_bar_global").style.display=hidden_div;
};
$("#menu_mobile").click(menu_control);
});
window.addEventListener(\'click\', function(e){
if (!document.getElementById(\'nav_bar_global\').contains(e.target) && !document.getElementById(\'menu_mobile\').contains(e.target) ){
document.getElementById("nav_bar_global").style.display="none";
}
});
window.addEventListener(\'touchstart\', function(e){
if (!document.getElementById(\'nav_bar_global\').contains(e.target) && !document.getElementById(\'menu_mobile\').contains(e.target) ){
document.getElementById("nav_bar_global").style.display="none";
}
});
$(window).resize(function() {
pro = document.getElementsByClassName("portail_pro");
par = document.getElementsByClassName("portail_par");
if ($(this).width() > 850) {
if (pro.length > 0) { for (i = 0; i < pro.length ; i++){ pro[i].hidden=true; } document.getElementById(\'par_p\').hidden = true }
if (par.length > 0) { for (i = 0; i < par.length ; i++){ par[i].hidden=true; } document.getElementById(\'pro_p\').hidden = true }
document.getElementById(\'img_type\').hidden= false;
document.getElementById(\'logo_heart_a\').href = "bienvenue.php";
} else {
if (pro.length > 0) { for (i = 0; i < pro.length ; i++){ pro[i].hidden=false; } document.getElementById(\'par_p\').hidden = false }
if (par.length > 0) { for (i = 0; i < par.length ; i++){ par[i].hidden=false; } document.getElementById(\'pro_p\').hidden = false }
document.getElementById(\'img_type\').hidden= true;
document.getElementById(\'logo_heart_a\').href = "accueil.php";
}
});
</script>';
// NB (synthèse §50) : le script du menu mobile de l'ancien site (nav_bar_global, menu_mobile, img_type, portail_pro,
// portail_par) a été retiré. Ces éléments n'existent plus : il provoquait une erreur JavaScript à chaque chargement de
// page et à chaque clic, et redirigeait le logo vers bienvenue.php / accueil.php, pages qui n'existent plus.
//--------------------------------------------------------------------------------------------
// Script d'affichage/masquage du menu :
//--------------------------------------------------------------------------------------------
+19 -16
View File
@@ -316,33 +316,36 @@ function Conversion_DECtoDMS($lat_lon, $valeur)
//************************************************************************************************************
// Fonction : Enregistrement dans le fichier de log de l'évènement
// Ne doit JAMAIS interrompre la page qui l'appelle (synthèse §50) : si le répertoire ou le fichier de log
// est inaccessible (cas du conteneur ship2net-php, où /home/koriolan/logs n'était pas monté), le message
// part dans le journal d'erreurs de PHP (visible par "docker logs ship2net-php") au lieu de provoquer une
// erreur fatale qui masquait l'erreur d'origine.
// Reçoit : Le texte à afficher dans le log, le fichier d'origine et la ligne d'origine
// Retour : Rien
//************************************************************************************************************
function Enregistrement_log($texte, $fichier, $ligne)
{
$repertoire_logs = REPERTOIRE_LOGS."/www_shipheart";
if (!file_exists($repertoire_logs) OR !is_dir($repertoire_logs)) // On vérifie d'abord que le répertoire existe, sinon on le créé.
{
mkdir($repertoire_logs);
$message_information .= "Le répertoire de logs [".$repertoire_logs."] n'existait pas. Il vient d'êtrer crééé<br />";
}
$fichier_log = $repertoire_logs.'/log_shipheart_'.date("Y-m-d").'.txt';
$prefixe_ligne = date("d/m/Y H:i:s")." [".basename($fichier)." - ".$ligne."] \t: ";
$suffixe_ligne = "\r\n";
$log = fopen($fichier_log, "a+"); // Ouverture en lecture/écriture en mode append...
fputs($log, $prefixe_ligne); // Entete de ligne
fputs($log, $texte); // Ecriture du texte de log
fputs($log, $suffixe_ligne); // Fin de ligne
fclose($log); // Fermetue du fichier.
// Répertoire créé au besoin (y compris les répertoires parents), sans avertissement en cas d'échec :
if (!is_dir($repertoire_logs)) @mkdir($repertoire_logs, 0775, true);
$log = @fopen($repertoire_logs.'/log_shipheart_'.date("Y-m-d").'.txt', "a");
if ($log !== false)
{
fputs($log, $prefixe_ligne.$texte.$suffixe_ligne);
fclose($log);
}
else
{
error_log("Ship2net (fichier de log inaccessible : ".$repertoire_logs.") ".$prefixe_ligne.$texte);
}
if(SHOW_DEBUG)
{
echo $prefixe_ligne . $texte . $suffixe_ligne;
echo htmlspecialchars($prefixe_ligne.$texte).$suffixe_ligne;
}
}