EDIT du 09/02/26:
Pour l’explication de la mise en place de la dernière version voir: PARTAGE prévisions nombres d'heures de soleil sur 7 jours en PHP - #28 par Noyax37
puis la dernière version du fichier à coller dans le bloc code 2:
Version initiale:
Excellent travail réalisé par @olive et tous les intervenants du post: https://community.jeedom.com/t/partage-previsions-nombres-dheures-de-soleil-sur-7-jours-script-python3/ qui permet de récupérer entre autre le nombre d’heure de soleil en prévision sur max 14 jours mais aussi d’autres infos météo du site Météo Dunkerque - meteoblue
N’ayant pas envie de reprendre tout l’historique des évolutions et pas trop envie non plus de m’embêter j’ai écrit un code à insérer dans un bloc code d’un scénario et qui met en forme un tableau exactement comme @pj66 l’a proposé. Le code récupère les données et va alimenter un virtuel qui, s’il n’existe pas, est créé automatiquement (resta à lui dire où l’afficher). On peut choisir le nombre de jours à extraire entre 1 et 14 puisque la page meteoblue propose jusqu’à 14 jours.
Merci pour le travail de fond qui a été fait, c’est top.
Voici de que ça donne avec une configuration pour extraire 7 jours, une pour 14 et une pour la journée:
Il vous reste juste à changer l’URL (vous l’aurez compris, j’habite à st avertin
):
// URL complète fournie → extraction automatique du nom de la ville et du code
$url = 'https://www.meteoblue.com/fr/meteo/semaine/saint-avertin_france_2981512';
à dire si vous voulez ou non des détails de la journée:
// à mettre sur false si on ne veut pas des infos détaillées de la journée
$detailJour = true;
à dire combien de jours vous voulez, entre 1 et 14:
$nb_jours_max = 7; // ←←← CHANGE ICI le nombre de jours souhaité (1 à 14)
Voici le code à coller dans un bloc code d’un scénario
// ────────────────────────────────────────────────
// CONFIGURATION
// ────────────────────────────────────────────────
// URL complète fournie → extraction automatique du nom de la ville et du code
$url = 'https://www.meteoblue.com/fr/meteo/semaine/saint-avertin_france_2981512';
// à mettre sur false si on ne veut pas des infos détaillées de la journée
$detailJour = true;
// Si vous n'êtes pas en France, changez _france_ ci dessous par le nom de votre pays
if (preg_match('#/semaine/([^/_]+)_france_(\d+)#i', $url, $matches)) {
$nomVille = $matches[1]; // saint-avertin
$codeVille = $matches[2]; // 2981512
$scenario->setLog("URL analysée → ville : $nomVille | code : $codeVille");
} else {
$scenario->setLog("ERREUR : impossible d'extraire ville et code depuis l'URL fournie");
$scenario->setLog("└── Script arrêté (format URL invalide)");
return;
}
// paramètres pour le plugin virtual
$nomVilleAffiche = ucwords(str_replace('-', ' ', $nomVille)); // si tiret remplace par un espace + majuscule, par exemple: saint-avertin → Saint Avertin
$nomAffiche = 'météo ' . $nomVilleAffiche; // → détermine le nom du virtuel, par exemple: météo Saint Avertin
// Construction du logicalId (format compatible Jeedom : minuscules, underscores) à adapter si vous avez des caractères spéciaux dans le nom de votre ville (au cas où...)
$logicalId = 'meteo_' . str_replace('-', '_', strtolower($nomVille)); // → meteo_saint_avertin
// et c'est parti
$nb_jours_max = 7; // ←←← CHANGE ICI le nombre de jours souhaité (1 à 14)
if (!is_int($nb_jours_max) || $nb_jours_max < 1 || $nb_jours_max > 14) {
$nb_jours_max = 7; // valeur par défaut sécurisée
$scenario->setLog("ATTENTION : nb_jours_max invalide → forcé à 7");
}
$scenario->setLog("┌── Début script Météo $nomVille ── ($nb_jours_max jours demandés)");
$url = "https://www.meteoblue.com/fr/meteo/semaine/{$nomVille}_france_{$codeVille}";
$scenario->setLog("→ URL : " . $url);
$scenario->setLog("Récupération page meteoblue...");
// Récupération via cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36');
$html = curl_exec($ch);
if ($html === false) {
$error = curl_error($ch);
$scenario->setLog("ERREUR cURL : " . $error);
curl_close($ch);
$scenario->setLog("└── Script arrêté (erreur réseau)");
return;
}
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$scenario->setLog("Code HTTP : " . $http_code . " | Taille HTML : " . strlen($html) . " octets");
if ($http_code != 200 || strlen($html) < 5000) {
$scenario->setLog("ERREUR : page invalide ou trop courte → abandon");
$scenario->setLog("└── Fin script (échec chargement page)");
return;
}
// Parsing DOM
$scenario->setLog("Parsing HTML...");
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
// Extraction jours et données
$jours_nodes = $xpath->query("//time[contains(concat(' ', normalize-space(@class), ' '), ' date ')]");
$jours_data_nodes = $xpath->query("//div[contains(concat(' ', normalize-space(@class), ' '), ' tab-content ')]");
$nb_jours_dispo = $jours_nodes->length;
$nb_data_dispo = $jours_data_nodes->length;
$nb_jours = min($nb_jours_max, $nb_jours_dispo);
$nb_data = min($nb_jours_max, $nb_data_dispo);
$scenario->setLog("Jours disponibles sur meteoblue : $nb_jours_dispo");
$scenario->setLog("Jours retenus (limite demandée) : $nb_jours");
$vent = $direction = $tmax = $tmin = $pluie = $soleil = [];
for ($i = 0; $i < $nb_data; $i++) {
$day = $jours_data_nodes->item($i);
// Vent
$vent_div_n = $xpath->query(".//div[contains(concat(' ', normalize-space(@class), ' '), ' wind ')]", $day);
if ($vent_div_n->length > 0) {
$vent_div = $vent_div_n->item(0);
$vitesse = trim(str_replace('km/h', '', $vent_div->textContent));
$span_n = $xpath->query(".//span", $vent_div);
$dir_val = ($span_n->length > 0) ? end(explode(' ', trim($span_n->item(0)->getAttribute('class')))) : 'N';
} else {
$vitesse = '0';
$dir_val = 'N';
}
$vent[] = $vitesse;
$direction[] = $dir_val;
// Autres valeurs
$node = $xpath->query(".//div[contains(@class,'tab-temp-max')]", $day)->item(0);
$tmax[] = $node ? trim($node->textContent) : '';
$node = $xpath->query(".//div[contains(@class,'tab-temp-min')]", $day)->item(0);
$tmin[] = $node ? trim($node->textContent) : '';
$node = $xpath->query(".//div[contains(@class,'tab-precip')]", $day)->item(0);
$pluie[] = $node ? trim($node->textContent) : '';
$node = $xpath->query(".//div[contains(@class,'tab-sun')]", $day)->item(0);
$soleil[] = $node ? trim($node->textContent) : '';
}
$scenario->setLog("Données extraites pour " . count($vent) . " jours");
if (count($vent) > 0) {
$scenario->setLog("Exemple jour 0 → Tmax: " . ($tmax[0] ?: '?') . " | Pluie: " . ($pluie[0] ?: '?'));
}
// ────────────────────────────────────────────────
// Fonctions utilitaires
// ────────────────────────────────────────────────
function wind_arrow($dir_val) {
$mapping = ["n"=>0,"nne"=>22.5,"ne"=>45,"ene"=>67.5,"e"=>90,"ese"=>112.5,"se"=>135,
"sse"=>157.5,"s"=>180,"sso"=>202.5,"ssw"=>202.5,"so"=>225,"sw"=>225,"oso"=>247.5,"wsw"=>247.5,
"o"=>270,"w"=>270,"ono"=>292.5,"wnw"=>292.5,"no"=>315,"nw"=>315,"nno"=>337.5,"nnw"=>337.5];
$lower = strtolower(trim($dir_val));
$angle = ($mapping[$lower] ?? 0) + 180;
$angle = $angle % 360;
return "<span style='display:inline-block; transform:rotate({$angle}deg); font-size:1.4em;'>↑</span>"
. "<span style='margin-left:5px; font-size:.75em; opacity:.7;'>" . strtoupper($dir_val) . "</span>";
}
function temp_color($temp) {
preg_match_all('/\d+\.?\d*/', $temp, $m);
$nums = array_map('floatval', $m[0]);
$s = $nums ? max($nums) : 0;
if ($s < 5) return "#6FA8FF";
if ($s <= 25 && $s>=5) return "#3CFF3C";
if ($s > 25) return "#FFA500";
return "#FF4040";
}
function vent_color($speed) {
preg_match_all('/\d+\.?\d*/', $speed, $m);
$nums = array_map('floatval', $m[0]);
$s = $nums ? max($nums) : 0;
if ($s < 30) return "#3CFF3C";
if ($s <= 50) return "#FFA500";
return "#FF4040";
}
function format_vent($value) {
preg_match_all('/\d+\.?\d*/', $value, $m);
$nums = $m[0];
if (count($nums) >= 2) {
return $nums[0] . " <span style='opacity:.7;'>↯" . end($nums) . "</span>";
}
return $value ?: '0';
}
function pluie_bg($value) {
preg_match_all('/\d+\.?\d*/', $value, $m);
$nums = array_map('floatval', $m[0]);
$p = $nums ? max($nums) : 0;
if ($p == 0) return "#3B003B";
if ($p <= 2) return "#FFD700";
if ($p <= 5) return "#FF8C00";
return "#FF0000";
}
function soleil_bar($value, $max_h = 10) {
preg_match_all('/\d+/', $value, $m);
$h = intval($m[0][0] ?? 0);
$ratio = min($h / $max_h, 1);
$width = intval($ratio * 100);
return "<div style='background:#333; border-radius:4px; height:10px; width:100%; margin:0 auto;'>"
. "<div style='background:#FFD700; height:100%; width:{$width}%; border-radius:4px;'></div></div>"
. "<div style='font-size:.85em; margin-top:2px; text-align:center;'>{$h} h</div>";
}
$COLORS = [
"Tmax" => "#FF6B5A",
"Tmin" => "#6FA8FF"
];
$ICONS = [
"Vent" => "💨",
"Direction" => "🧭",
"Tmax" => "⬆️",
"Tmin" => "⬇️",
"Pluie" => "🌧️",
"Soleil" => "☀️"
];
// ────────────────────────────────────────────────
// Construction du tableau HTML
// ────────────────────────────────────────────────
$table = "<table style='border-collapse:separate; border-spacing:4px 2px; font-family:sans-serif; font-size:14px;'>";
// Ligne des jours
$table .= "<tr><td style='font-weight:bold; padding:6px; min-width:70px;'>Jours :</td>";
$count_jours = 0;
foreach ($jours_nodes as $j) {
if ($count_jours >= $nb_jours) {
break;
}
$short_node = $xpath->query(".//div[contains(concat(' ', normalize-space(@class), ' '), ' tab-day-short ')]", $j)->item(0);
$short_day = $short_node ? trim($short_node->textContent) : '';
$long_node = $xpath->query(".//div[contains(concat(' ', normalize-space(@class), ' '), ' tab-day-long ')]", $j)->item(0);
$long_day = $long_node ? trim($long_node->textContent) : '';
$table .= "<td style='font-weight:bold; text-align:center; padding:6px; min-width:70px;'>"
. htmlspecialchars($short_day) . "<br>"
. "<span style='font-size:0.8em; opacity:.7;'>" . htmlspecialchars($long_day) . "</span></td>";
$count_jours++;
}
$table .= "</tr>";
// Lignes des données
$rows = [
"Vent" => $vent,
"Direction" => $direction,
"Tmax" => $tmax,
"Tmin" => $tmin,
"Pluie" => $pluie,
"Soleil" => $soleil
];
$row_index = 0;
foreach ($rows as $label => $values) {
$bg = ($row_index % 2 == 0) ? "transparent" : "rgba(255,255,255,0.04)";
$icon = $ICONS[$label] ?? '';
$table .= "<tr><td style='background-color:{$bg}; font-weight:bold; padding:6px; min-width:70px;'>{$icon} {$label} :</td>";
for ($col = 0; $col < $nb_jours; $col++) {
$v = $values[$col] ?? ''; // sécurité si tableau plus court
$cell_bg = $bg;
$text_color = "white";
$display = htmlspecialchars($v);
$size = in_array($label, ["Tmax", "Tmin"]) ? "1.1em" : "1em";
if ($label == "Vent") {
$display = format_vent($v);
$text_color = vent_color($v);
} elseif ($label == "Direction") {
$display = wind_arrow($direction[$col] ?? 'N');
$text_color = vent_color($vent[$col] ?? '0');
} elseif ($label == "Pluie") {
$display = preg_replace('/(mm)/', "<span style='font-size:0.88em;'>$1</span>", htmlspecialchars($v));
$cell_bg = pluie_bg($v);
$text_color = "black";
} elseif ($label == "Soleil") {
$display = soleil_bar($v);
} else {
$text_color = $COLORS[$label] ?? "white";
}
$table .= "<td style='background-color:{$cell_bg}; color:{$text_color}; text-align:center; font-weight:bold; padding:6px; font-size:{$size}; min-width:70px;'>"
. $display . "</td>";
}
$table .= "</tr>";
$row_index++;
}
$table .= "</table>";
$scenario->setLog("Tableau HTML généré (" . strlen($table) . " caractères)");
// ────────────────────────────────────────────────
// 2. Tableau détaillé aujourd'hui (three-hourly-view)
// ────────────────────────────────────────────────
if ($detailJour){
$today_content = "<p style='color:#f66;font-weight:bold;margin:0;padding:0;'>Tableau horaire introuvable</p>";
$three_hourly = $xpath->query("//table[contains(@class,'picto') and contains(@class,'three-hourly-view')]");
if ($three_hourly->length > 0) {
$tbl = $three_hourly->item(0);
$time_cells = $xpath->query(".//tr[contains(@class,'times')]//td", $tbl);
$icon_cells = $xpath->query(".//tr[contains(@class,'icons')]//img", $tbl);
$temp_cells = $xpath->query(".//tr[contains(@class,'temperatures')]//td//div[contains(@class,'cell')]", $tbl);
$feel_cells = $xpath->query(".//tr[contains(@class,'windchills')]//td//div[contains(@class,'cell')]", $tbl);
$wind_cells = $xpath->query(".//tr[contains(@class,'windspeeds')]//td//div[contains(@class,'cell')]", $tbl);
$dir_cells = $xpath->query(".//tr[contains(@class,'winddirs')]//td//div[contains(@class,'winddir')]", $tbl);
$pluie_cells = $xpath->query(".//tr[contains(@class,'precipprobs')]//td//span[contains(@class,'precip-prob')]", $tbl);
$today_content = '<div style="margin:0;padding:0;max-width:100%;overflow-x:auto;">'
. '<table style="width:100%;max-width:100%;border-collapse:collapse;font-size:12px;text-align:center;margin:0;padding:0;border-spacing:0;">'
. '<thead><tr style="background:#2c2c2c;color:#eee;">'
. '<th style="padding:4px 3px;font-size:11px;text-align=center;">Heure</th>'
. '<th style="padding:4px 3px;font-size:11px;text-align=center;">Icône</th>'
. '<th style="padding:4px 3px;font-size:11px;text-align=center;">Temp</th>'
. '<th style="padding:4px 3px;font-size:11px;text-align=center;">Ressenti</th>'
. '<th style="padding:4px 3px;font-size:11px;text-align=center;">Vent</th>'
. '<th style="padding:4px 3px;font-size:11px;text-align=center;">Pluie</th>'
. '</tr></thead><tbody>';
$slots = min(8, $time_cells->length);
for ($i = 0; $i < $slots; $i++) {
$h_node = $xpath->query(".//time", $time_cells->item($i))->item(0);
$h = $h_node ? trim($h_node->textContent) : '?';
$icon = $icon_cells->item($i) ?? null;
$icon_html = $icon
? '<img src="' . htmlspecialchars($icon->getAttribute('src')) . '" alt="' . htmlspecialchars($icon->getAttribute('alt') ?? '') . '" style="max-width:28px;height:auto;vertical-align:middle;">'
: '-';
$temp = trim($temp_cells->item($i)->textContent ?? '-');
$ressenti = trim($feel_cells->item($i)->textContent ?? '-');
$vent = trim($wind_cells->item($i)->textContent ?? '-');
$dir_node = $dir_cells->item($i);
$dir = $dir_node ? trim($dir_node->textContent) : '-';
$pluie = trim($pluie_cells->item($i)->textContent ?? '0%');
$vent_display = ($vent !== '-' && $dir !== '-')
? $vent . ' ' . wind_arrow($dir)
: ($vent !== '-' ? $vent : $dir);
$moyvent = '-';
if(preg_match('/(\d+)\s*-\s*(\d+)/', $vent, $matches)) {
$moyVent = ($matches[1] + $matches[2]) / 2;
}
$vent_color = ($moyVent !== '-') ? vent_color($moyVent) : 'transparent';
$temp_color = ($temp !== '-') ? temp_color($temp) : 'transparent';
$temp_color_ressenti = ($ressenti !== '-') ? temp_color($ressenti) : 'transparent';
$today_content .= '<tr style="border-bottom:1px solid #444;height:32px;margin:0;padding:0;">'
. '<td style="padding:4px 3px;margin:0;">' . htmlspecialchars($h) . '</td>'
. '<td style="padding:4px 3px;margin:0;">' . $icon_html . '</td>'
. '<td style="padding:4px 3px;margin:0;color:' . $temp_color . '">' . htmlspecialchars($temp) . '</td>'
. '<td style="padding:4px 3px;margin:0;color:' . $temp_color_ressenti . '">' . htmlspecialchars($ressenti) . '</td>'
. '<td style="padding:4px 3px;margin:0;color:' . $vent_color . '">' . $vent_display . '</td>'
. '<td style="padding:4px 3px;margin:0;">' . htmlspecialchars($pluie) . '</td>'
. '</tr>';
}
$today_content .= '</tbody></table></div>';
$scenario->setLog("Tableau today complet reconstruit ($slots créneaux)");
} else {
$scenario->setLog("Aucun tableau three-hourly-view trouvé");
}
}
// ────────────────────────────────────────────────
// Gestion équipement virtuel
// ────────────────────────────────────────────────
function findVirtual($name) {
$name = strtolower(trim($name));
foreach (eqLogic::byType('virtual') as $eq) {
if (strtolower(trim($eq->getName())) === $name) {
return $eq;
}
}
return null;
}
// Principal
$virtual = findVirtual($nomAffiche);
if (!$virtual) {
$virtual = new eqLogic();
$virtual->setName($nomAffiche);
$virtual->setLogicalId($logicalId);
$virtual->setEqType_name('virtual');
$virtual->setIsEnable(1);
$virtual->setIsVisible(1);
$virtual->save();
$cmd = new cmd();
$cmd->setEqLogic_id($virtual->getId());
$cmd->setLogicalId('tableau_meteo');
$cmd->setName('Tableau Météo');
$cmd->setType('info');
$cmd->setSubType('string');
$cmd->save();
}
$cmdMain = $virtual->getCmd('info', 'tableau_meteo');
if ($cmdMain) {
$virtual->checkAndUpdateCmd('tableau_meteo', $table);
$virtual->refreshWidget();
$scenario->setLog("Principal mis à jour");
}
// Today
if ($detailJour){
$nomToday = $nomAffiche . ' - détaillé';
$logicalToday = $logicalId . '_today';
$virtualToday = findVirtual($nomToday);
if (!$virtualToday) {
$virtualToday = new eqLogic();
$virtualToday->setName($nomToday);
$virtualToday->setLogicalId($logicalToday);
$virtualToday->setEqType_name('virtual');
$virtualToday->setIsEnable(1);
$virtualToday->setIsVisible(1);
$virtualToday->save();
$cmdT = new cmd();
$cmdT->setEqLogic_id($virtualToday->getId());
$cmdT->setLogicalId('tableau_today');
$cmdT->setName('Aujourd\'hui détaillé');
$cmdT->setType('info');
$cmdT->setSubType('string');
$cmdT->save();
}
$cmdToday = $virtualToday->getCmd('info', 'tableau_today');
if ($cmdToday) {
$virtualToday->checkAndUpdateCmd('tableau_today', $today_content);
$virtualToday->refreshWidget();
$scenario->setLog("Today mis à jour");
}
}
$scenario->setLog("└── Fin script");



