Bonjour à tous, j’avais fait il y a quelques temps un programme python pour aller chercher les infos des batteries Pylontech installées chez moi.
Alors ça fonctionne bien mais le paramétrage est pénible. J’ai donc eu dans l’intention pendant assez longtemps d’utiliser le bloc code des scénarios pour voir si on pouvait en tirer qq chose.
Pour mémoire ou si vous n’êtes pas allés lire le post cité plus haut, j’ai un convertisseur série / Ethernet de la marque Hy Fly le HF2221. Peu importe la marque et le modèle car sur le principe il faut un convertisseur qui permette d’interroger via le réseau en TCP (je ne sais pas trop si on dit comme ça).
Voici le code que je vous propose (bon j’avoue que pour certains points Grok m’a un peu filé un coup de main
):
// Constantes de connexion
const IP = "192.168.1.18"; // Adresse IP du HF2221
const PORT = 8899; // Port TCP
const TIMEOUT = 10; // Timeout en secondes
// Constantes de personnalisation des noms
const VIRTUAL_GLOBAL = "Global batteries";
const VIRTUAL_BAT_PREFIX = "batterie ";
const CMD_VOLTAGE = "Tension";
const CMD_CURRENT = "Intensité";
const CMD_TEMP = "Temp";
const CMD_CAPACITY = "Capacité";
const CMD_SOC = "SOC";
const CMD_CYCLES = "Cycles"; // Cycles par batterie (de getpwr)
const CMD_PACK_CYCLES = "Cycles Pack"; // Cycles totaux du pack (de pwr 1)
const CMD_PACK_DISCHARGE_TIME = "Temps Décharge Pack"; // Temps de décharge du pack (de pwr 1)
const CMD_ERROR = "Erreur TimeOut"; // Compteur de timeouts consécutifs
const CMD_COUNT = "Nb Bat";
const CMD_AVG_VOLTAGE = "Tension Moyenne";
const CMD_TOTAL_CURRENT = "Intensité totale";
const CMD_AVG_SOC = "SOC moyen";
const CMD_CELL_VOLTAGE = "U";
const CMD_CELL_TEMP = "T";
const CMD_STATE = "État";
const CMD_SOH = "SOH";
// Fonction pour envoyer une commande et lire la réponse
function sendCommand($socket, string $commande, $scenario): string {
fwrite($socket, $commande);
$scenario->setLog("Commande envoyée : " . trim($commande));
sleep(2);
stream_set_blocking($socket, false);
$reponse = '';
for ($i = 0; $i < 5 && !feof($socket); $i++) {
$donnees = fread($socket, 2048);
if ($donnees !== false && strlen($donnees) > 0) {
$reponse .= $donnees;
break;
}
$scenario->setLog("Tentative $i : rien lu");
sleep(1);
}
return $reponse;
}
// Fonction pour créer ou mettre à jour un équipement virtuel
function updateVirtual(string $nom, array $commandes, $scenario): void {
$virtual = eqLogic::byLogicalId($nom, 'virtual');
if (!is_object($virtual)) {
$virtual = new eqLogic();
$virtual->setEqType_name('virtual');
$virtual->setLogicalId($nom);
$virtual->setName($nom);
$virtual->setIsEnable(1);
$virtual->setIsVisible(1);
$virtual->save();
$scenario->setLog("Équipement virtuel '$nom' créé");
}
foreach ($commandes as $cmd_name => $valeur) {
$cmd = $virtual->getCmd(null, $cmd_name);
if (!is_object($cmd)) {
$cmd = new virtualCmd();
$cmd->setEqLogic_id($virtual->getId());
$cmd->setLogicalId($cmd_name);
$cmd->setName($cmd_name);
$cmd->setType('info');
$subtype = is_numeric($valeur) ? 'numeric' : 'string';
$cmd->setSubType($subtype);
$cmd->setIsHistorized($subtype === 'numeric' ? 1 : 0);
switch (true) {
case strpos($cmd_name, CMD_VOLTAGE) === 0 || strpos($cmd_name, CMD_CELL_VOLTAGE) === 0: $cmd->setUnite('V'); break;
case strpos($cmd_name, CMD_CURRENT) === 0: $cmd->setUnite('A'); break;
case strpos($cmd_name, CMD_TEMP) === 0 || strpos($cmd_name, CMD_CELL_TEMP) === 0: $cmd->setUnite('°C'); break;
case strpos($cmd_name, CMD_SOC) === 0: $cmd->setUnite('%'); break;
case $cmd_name === CMD_CAPACITY: $cmd->setUnite('Ah'); break;
case $cmd_name === CMD_PACK_DISCHARGE_TIME: $cmd->setUnite('s'); break;
}
$cmd->save();
$scenario->setLog("Commande '$cmd_name' ajoutée à '$nom'");
}
$virtual->checkAndUpdateCmd($cmd_name, $valeur);
}
}
// Vérification du plugin Virtuel
$pluginVirtual = plugin::byId('virtual');
if (!is_object($pluginVirtual) || !$pluginVirtual->isActive()) {
$scenario->setLog("Erreur : Plugin 'Virtuel' non installé ou désactivé.");
return;
}
$scenario->setLog("Plugin 'Virtuel' actif");
// Récupérer la valeur actuelle de CMD_ERROR
$virtual_global = eqLogic::byLogicalId(VIRTUAL_GLOBAL, 'virtual');
$cmd_error = is_object($virtual_global) ? $virtual_global->getCmd(null, CMD_ERROR) : null;
$error_flag = is_object($cmd_error) ? (int)$cmd_error->execCmd() : 0;
$had_timeout = false; // Indicateur pour savoir si un timeout a eu lieu dans cette exécution
// Connexion au HF2221
$scenario->setLog("Connexion à " . IP . ":" . PORT);
$socket = @fsockopen(IP, PORT, $errno, $errstr, TIMEOUT);
if (!$socket) {
$error_flag++;
$had_timeout = true;
$scenario->setLog("Erreur de connexion : $errstr ($errno)");
updateVirtual(VIRTUAL_GLOBAL, [CMD_ERROR => $error_flag], $scenario);
return;
}
$scenario->setLog("Connexion établie");
// Lecture de l'heure avec "time"
$reponse_time = sendCommand($socket, "time\r\n", $scenario);
$system_time = null;
if (!empty($reponse_time)) {
if (preg_match('/Ds3231\s+(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/', $reponse_time, $match)) {
$system_time = mktime($match[4], $match[5], $match[6], $match[2], $match[3], $match[1]);
$scenario->setLog("Heure système : " . $match[1] . "-" . $match[2] . "-" . $match[3] . " " . $match[4] . ":" . $match[5] . ":" . $match[6]);
}
} else {
$error_flag++;
$had_timeout = true;
$scenario->setLog("Timeout ou erreur sur 'time'");
}
// Vérification et mise à l'heure si décalage > 1h
$local_time = time();
if ($system_time !== null) {
$time_diff = abs($local_time - $system_time);
$scenario->setLog("Décalage horaire : $time_diff secondes");
if ($time_diff > 3600) {
$date = getdate($local_time);
$year = substr($date['year'], -2);
$month = sprintf("%02d", $date['mon']);
$day = sprintf("%02d", $date['mday']);
$hour = sprintf("%02d", $date['hours']);
$minute = sprintf("%02d", $date['minutes']);
$second = sprintf("%02d", $date['seconds']);
$time_command = "time $year $month $day $hour $minute $second\r\n";
$reponse_set_time = sendCommand($socket, $time_command, $scenario);
if (empty($reponse_set_time)) {
$error_flag++;
$had_timeout = true;
$scenario->setLog("Timeout sur mise à l'heure");
} else {
$scenario->setLog("Mise à l'heure effectuée : $year-$month-$day $hour:$minute:$second");
}
} else {
$scenario->setLog("Pas de mise à l'heure nécessaire (décalage < 1h)");
}
} else {
$scenario->setLog("Heure système non lue, pas de mise à l'heure effectuée");
}
// Étape 1 : Récupérer l'état global (pwr)
$reponse_pwr = sendCommand($socket, "pwr\r\n", $scenario);
if (empty($reponse_pwr)) {
$error_flag++;
$had_timeout = true;
$scenario->setLog("Aucune donnée reçue pour 'pwr' (timeout)");
updateVirtual(VIRTUAL_GLOBAL, [CMD_ERROR => $error_flag], $scenario);
fclose($socket);
return;
}
$scenario->setLog("Réponse 'pwr' reçue");
// Parser les données de "pwr"
$lignes_pwr = array_filter(explode("\n", $reponse_pwr), 'strlen');
$batteries_installees = 0;
$total_volt = $total_curr = $total_soc = 0;
foreach ($lignes_pwr as $ligne) {
$valeurs = array_filter(explode(" ", preg_replace('/\s+/', ' ', trim($ligne))), 'strlen');
if (count($valeurs) >= 15 && is_numeric($valeurs[0]) && $valeurs[0] >= 1 && $valeurs[0] <= 8 && $valeurs[8] !== "Absent") {
$id = (int)$valeurs[0];
$batteries_installees = max($batteries_installees, $id);
$volt = $valeurs[1] == "-" ? 0 : $valeurs[1] / 1000;
$curr = $valeurs[2] == "-" ? 0 : $valeurs[2] / 1000;
$temp = $valeurs[3] == "-" ? 0 : $valeurs[3] / 1000;
$soc = (float)rtrim($valeurs[12], '%');
updateVirtual(VIRTUAL_GLOBAL, [
CMD_VOLTAGE . $id => $volt,
CMD_CURRENT . $id => $curr,
CMD_SOC . $id => $soc,
CMD_TEMP . $id => $temp,
], $scenario);
$total_volt += $volt;
$total_curr += $curr;
$total_soc += $soc;
}
}
$scenario->setLog("Batteries détectées : $batteries_installees");
if ($batteries_installees > 0) {
$moyenne_volt = $total_volt / $batteries_installees;
$moyenne_soc = $total_soc / $batteries_installees;
updateVirtual(VIRTUAL_GLOBAL, [
CMD_AVG_VOLTAGE => $moyenne_volt,
CMD_TOTAL_CURRENT => $total_curr,
CMD_AVG_SOC => $moyenne_soc,
CMD_COUNT => $batteries_installees,
CMD_ERROR => $error_flag, // Toujours mis à jour ici
], $scenario);
$scenario->setLog(sprintf("Global : %.2f V, %.2f A, %.1f%%, Erreur: %d", $moyenne_volt, $total_curr, $moyenne_soc, $error_flag));
}
// Variables pour stocker les données globales du pack (de pwr 1)
$pack_cycles = 0;
$pack_discharge_time = 0;
// Étape 2 : Détails par batterie (pwr X + getpwr X)
for ($i = 1; $i <= $batteries_installees; $i++) {
// Récupérer pwr X
$reponse_pwr = sendCommand($socket, "pwr $i\r\n", $scenario);
if (empty($reponse_pwr)) {
$error_flag++;
$had_timeout = true;
$scenario->setLog("Aucune donnée pour 'pwr $i' (timeout)");
continue;
}
$scenario->setLog("Réponse 'pwr $i' reçue");
$lignes_pwr = array_filter(explode("\n", $reponse_pwr), 'strlen');
$commandes = [];
foreach ($lignes_pwr as $ligne) {
$ligne = trim($ligne);
if (preg_match('/Voltage\s*:\s*(\d+)\s*mV/', $ligne, $match)) {
$commandes[CMD_VOLTAGE] = $match[1] / 1000;
} elseif (preg_match('/Current\s*:\s*(-?\d+)\s*mA/', $ligne, $match)) {
$commandes[CMD_CURRENT] = $match[1] / 1000;
} elseif (preg_match('/Temperature\s*:\s*(\d+)\s*mC/', $ligne, $match)) {
$commandes[CMD_TEMP] = $match[1] / 1000;
} elseif (preg_match('/Coulomb\s*:\s*(\d+)\s*%/', $ligne, $match)) {
$commandes[CMD_SOC] = (int)$match[1];
} elseif (preg_match('/Total Coulomb\s*:\s*(\d+)\s*mAH/', $ligne, $match)) {
$commandes[CMD_CAPACITY] = $match[1] / 1000;
} elseif (preg_match('/Charge Times\s*:\s*(\d+)/', $ligne, $match)) {
$pack_cycles = (int)$match[1];
} elseif (preg_match('/Basic Status\s*:\s*(\w+)/', $ligne, $match)) {
$commandes[CMD_STATE] = $match[1];
} elseif (preg_match('/Discharge Sec\.\s*:\s*(\d+)\s*s/', $ligne, $match)) {
$pack_discharge_time = (int)$match[1];
} elseif (preg_match('/Soh\. Status\s*:\s*(\w+)/', $ligne, $match)) {
$commandes[CMD_SOH] = $match[1];
}
}
// Récupérer getpwr X pour les cellules et cycles individuels
$reponse_getpwr = sendCommand($socket, "getpwr $i\r\n", $scenario);
if (!empty($reponse_getpwr)) {
$lignes_getpwr = array_filter(explode("\n", $reponse_getpwr), 'strlen');
$elements = 0;
$parsed = false;
foreach ($lignes_getpwr as $ligne) {
$valeurs = array_filter(explode("#", trim($ligne)), 'strlen');
if (count($valeurs) >= 8 && !$parsed) {
$commandes[CMD_VOLTAGE] = $valeurs[0] / 1000;
$commandes[CMD_CURRENT] = $valeurs[1] / 1000;
$commandes[CMD_TEMP] = $valeurs[2] / 1000;
$parsed = true;
} elseif (count($valeurs) >= 4 && $parsed && $elements < 15) {
$elements++;
$commandes[CMD_CELL_VOLTAGE . $elements] = $valeurs[0] / 1000;
$commandes[CMD_CELL_TEMP . $elements] = $valeurs[1] / 1000;
if ($elements <= 3) {
$scenario->setLog(sprintf("Batterie %d - Élément %d : %.3f V, %.1f °C", $i, $elements, $commandes[CMD_CELL_VOLTAGE . $elements], $commandes[CMD_CELL_TEMP . $elements]));
}
} elseif (count($valeurs) == 1 && is_numeric($valeurs[0]) && $valeurs[0] > 0 && $elements == 15) {
$commandes[CMD_CYCLES] = (int)$valeurs[0];
}
}
if ($elements < 15) {
$scenario->setLog("Batterie $i - Attention : $elements éléments détectés (15 attendus)");
}
} else {
$error_flag++;
$had_timeout = true;
$scenario->setLog("Aucune donnée pour 'getpwr $i' (timeout)");
}
// Log des données principales
if (!empty($commandes[CMD_VOLTAGE])) {
$scenario->setLog(sprintf("Batterie %d : %.3f V, %.3f A, %.1f °C, %.2f Ah, %d%%, %s, SOH: %s, Cycles: %d",
$i, $commandes[CMD_VOLTAGE], $commandes[CMD_CURRENT], $commandes[CMD_TEMP],
$commandes[CMD_CAPACITY], $commandes[CMD_SOC], $commandes[CMD_STATE],
$commandes[CMD_SOH], $commandes[CMD_CYCLES] ?? 0));
}
updateVirtual(VIRTUAL_BAT_PREFIX . $i, $commandes, $scenario);
}
// Ajouter les données globales du pack à Global batteries
if ($batteries_installees > 0) {
// Si pas de timeout dans cette exécution, réinitialiser à 0
$error_flag = $had_timeout ? $error_flag : 0;
$global_data = [CMD_ERROR => $error_flag]; // Toujours inclus
if ($pack_cycles > 0) $global_data[CMD_PACK_CYCLES] = $pack_cycles;
if ($pack_discharge_time > 0) $global_data[CMD_PACK_DISCHARGE_TIME] = $pack_discharge_time;
updateVirtual(VIRTUAL_GLOBAL, $global_data, $scenario);
$scenario->setLog("Pack : Cycles = $pack_cycles, Temps Décharge = $pack_discharge_time s");
} else {
// Si aucune batterie détectée mais connexion OK, forcer Erreur TimeOut à être mis à jour
updateVirtual(VIRTUAL_GLOBAL, [CMD_ERROR => $error_flag], $scenario);
}
// Fermeture
fclose($socket);
$scenario->setLog("Connexion fermée");
Alors il faut donc créer un scénario, programmer son exécution, ajouter un bloc code et y coller ce que vous avez ci dessus.
Au début du code vous trouverez toutes les constantes vous permettant d’adapter le résultat à ce que vous souhaitez.
Il vous faudra à minima paramétrer l’IP de votre convertisseur et le port TCP qu’il utilise comme serveur vis à vis de la connexion série. Le reste sert pour les appellations des batterie, des tensions, … A vous de voir
A la 1ère utilisation le code va créer un virtuel reprenant le global (appelé « Global Batterie » dans mon exemple) et autant de batteries que celles qui sont installées. Dans chaque virtuel il créera les commandes retrouvées lors de sa scrutation. Voici par exemple chez moi où j’ai 4 batteries installées le virtuel « Global Batterie »:
Et en exemple la première batterie:
La commande « Dernière mise à jour » je l’ai rajoutée à la main, elle ne figure pas dans le code.
L’heure des batteries est testée et si la différence est supérieure à 1h alors la mise à jour est faite.
Le code teste et comptabilise les « time out » car sur mon hf2221 j’ai remarqué que parfois il se bloque et il faut l’éteindre et le rallumer pour que ça reparte. J’ai donc installé un sonoff R2 hacké pour le commander à OFF si le time out et >2 (attention si vous faites ça car à la 1ère ré interrogation des batterie il y a 4 time out généré et je n’ai pas cherché à les enlever, il faut donc en tenir compte dans votre scénario éventuel) mais ça c’est chacun qui voit ![]()
Bon code ![]()
PS: si vous aviez déjà utilisé mon précédent code en python il faut que vous repartiez avec des noms de virtuels différents. Comme le nouveau code se base sur les logicalId qui n’ont pas forcément été crées cela fait des erreurs et je n’ai pas réussi facilement à m’en sortir j’ai donc laissé tomber l’idée de récupérer les anciens virtuels. Si vous avez des historiques à conserver vous pouvez toujours les transférer après.



