PARTAGE prévisions nombres d'heures de soleil sur 7 jours en PHP

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 :wink: ):

// 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");
1 « J'aime »

ça marche super bien sur mon jeedom de dev et sur celui de prod j’ai une erreur quand je veux enregistrer le bloc code. Apparemment ça vient des icones :

$ICONS = [
    "Vent" => "💨",
    "Direction" => "🧭",
    "Tmax" => "⬆️",
    "Tmin" => "⬇️",
    "Pluie" => "🌧️",
    "Soleil" => "☀️"
];

à remplacer par ce qui suit si jamais cela vous arrive en attendant que quelqu’un trouve une solution:

$ICONS = [
    "Vent" => "",
    "Direction" => "",
    "Tmax" => "",
    "Tmin" => "",
    "Pluie" => "",
    "Soleil" => ""
];

C’est une histoire d’utf8, voici le code à mettre un place:

$ICONS = [
    "Vent" => "\u{1F4A8}", 
    "Direction" => "\u{1F9ED}",
    "Tmax" => "\u{2B06}\u{FE0F}", 
    "Tmin" => "\u{2B07}\u{FE0F}", 
    "Pluie" => "\u{1F327}\u{FE0F}",
    "Soleil" => "\u{2600}\u{FE0F}" 
];

1 « J'aime »

Hello,

Superbe modification , j’essaierai ton nouveau code
Et courage pour toute cette pluie à partir de mardi par chez toi :face_with_hand_over_mouth:

1 « J'aime »

Même pas peur :stuck_out_tongue_winking_eye:

j’ai un peu fait évoluer la prévision journalière:

Quand j’aurai un peu plus de temps je ferai l’extraction à l’heure des précipitations

le code:

// ────────────────────────────────────────────────
// 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 <= 0) return "#3828c7";
    if ($s <= 5) return "#6FA8FF";
    if ($s <= 15) return "#b3e146";
    if ($s <= 20) return "#3CFF3C";
    if ($s <= 28) 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 "#a562a5";
    if ($p <= 2) return "#FFD700";
    if ($p <= 5) return "#FF8C00";
    return "#FF0000";
}

function pluie_percent($value) {
    preg_match_all('/\d+\.?\d*/', $value, $m);
    $nums = array_map('floatval', $m[0]);
    $p = $nums ? max($nums) : 0;
    if ($p == 0) return "#a562a5";
    if ($p <= 20) return "#FFD700";
    if ($p <= 50) 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" => "\u{1F4A8}",
    "Direction" => "\u{1F9ED}",     
    "Tmax" => "\u{2B06}\u{FE0F}",  
    "Tmin" => "\u{2B07}\u{FE0F}",   
    "Pluie" => "\u{1F327}\u{FE0F}", 
    "Soleil" => "\u{2600}\u{FE0F}" 
];


// ────────────────────────────────────────────────
// 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] ?? 'rgb(var(--txt-color))';
        }

        $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);
      $pluie_mm    = $xpath->query(".//tr[contains(@class,'precips')]//td//div[contains(@class,'cell')]//span", $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>'
                     . '<th style="padding:4px 3px;font-size:11px;text-align=center;">(mm)</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) : '?';

          if ($h === '0300') {
            $h = '0-3h';
          } elseif ($h === '0600') {
            $h = '3-6h';
          } elseif ($h === '0900') {
            $h = '6-9h';
          } elseif ($h === '1200') {
            $h = '9-12h';
          } elseif ($h === '1500') {
            $h = '12-15h';
          } elseif ($h === '1800') {
            $h = '15-18h';
          } elseif ($h === '2100') {
            $h = '18-21h';
          } elseif ($h === '2400') {
            $h = '21-24h';
          }

          $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%');
          $pluie_extract_mm  = trim($pluie_mm->item($i)->textContent ?? '0 mm');

          $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) : 'rgb(var(--txt-color))';

          $temp_color = ($temp !== '-') ? temp_color($temp) : 'rgb(var(--txt-color))';

          $temp_color_ressenti = ($ressenti !== '-') ? temp_color($ressenti) : 'rgb(var(--txt-color))';

          $pluie_color = pluie_percent($pluie);
          $pluie_mm_color = pluie_bg($pluie_extract_mm);

          $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;color:' . $pluie_color . '">' . htmlspecialchars($pluie) . '</td>'
                         . '<td style="padding:4px 3px;margin:0;color:' . $pluie_mm_color . '">' . htmlspecialchars($pluie_extract_mm) . '</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");
1 « J'aime »

Je vais peut être faire un post séparé pour éviter de polluer ici car c’est normalement du python dont il est question.

Je ne sais pas mais j’ai pensé (j’aurai dû certainement le faire avant…) que certains pouvaient vouloir récupérer les données recueillies pour y appliquer des actions. J’ai donc modifié le code pour enregistrer les données dans une commande qui va stocker le json des données extraites.

Cela va donner ça pour le tableau sur 7 jours:

[
  {
    "jour_court": "ven.",
    "jour_long": "Aujourd'hui",
    "donnees": {
      "Vent": "23",
      "Direction": "SW",
      "Tmax": "12 °C",
      "Tmin": "7 °C",
      "Pluie": "0-2 mm",
      "Soleil": "3 h"
    }
  },
  {
    "jour_court": "sam.",
    "jour_long": "Demain",
    "donnees": {
      "Vent": "23",
      "Direction": "S",
      "Tmax": "11 °C",
      "Tmin": "6 °C",
      "Pluie": "0-2 mm",
      "Soleil": "2 h"
    }
  },
  {
    "jour_court": "dim.",
    "jour_long": "8/2",
    "donnees": {
      "Vent": "9",
      "Direction": "NE",
      "Tmax": "11 °C",
      "Tmin": "5 °C",
      "Pluie": "-",
      "Soleil": "0 h"
    }
  },
  {
    "jour_court": "lun.",
    "jour_long": "9/2",
    "donnees": {
      "Vent": "15",
      "Direction": "S",
      "Tmax": "11 °C",
      "Tmin": "4 °C",
      "Pluie": "5-10 mm",
      "Soleil": "1 h"
    }
  },
  {
    "jour_court": "mar.",
    "jour_long": "10/2",
    "donnees": {
      "Vent": "23",
      "Direction": "SW",
      "Tmax": "12 °C",
      "Tmin": "8 °C",
      "Pluie": "5-10 mm",
      "Soleil": "0 h"
    }
  },
  {
    "jour_court": "mer.",
    "jour_long": "11/2",
    "donnees": {
      "Vent": "25",
      "Direction": "SW",
      "Tmax": "12 °C",
      "Tmin": "9 °C",
      "Pluie": "10-20 mm",
      "Soleil": "3 h"
    }
  },
  {
    "jour_court": "jeu.",
    "jour_long": "12/2",
    "donnees": {
      "Vent": "26",
      "Direction": "SW",
      "Tmax": "12 °C",
      "Tmin": "9 °C",
      "Pluie": "5-10 mm",
      "Soleil": "1 h"
    }
  }
]

Et pour le tableau détaillé:

{
  "0-3h": {
    "temp": "10°",
    "ressenti": "6°",
    "vent": "19-35",
    "direction": "SO",
    "pluie_percent": "65%",
    "pluie_mm": "-"
  },
  "3-6h": {
    "temp": "10°",
    "ressenti": "5°",
    "vent": "20-40",
    "direction": "SO",
    "pluie_percent": "65%",
    "pluie_mm": "< 1"
  },
  "6-9h": {
    "temp": "10°",
    "ressenti": "5°",
    "vent": "21-38",
    "direction": "SO",
    "pluie_percent": "20%",
    "pluie_mm": "-"
  },
  "9-12h": {
    "temp": "10°",
    "ressenti": "5°",
    "vent": "23-44",
    "direction": "OSO",
    "pluie_percent": "65%",
    "pluie_mm": "< 1"
  },
  "12-15h": {
    "temp": "11°",
    "ressenti": "7°",
    "vent": "22-49",
    "direction": "OSO",
    "pluie_percent": "65%",
    "pluie_mm": "< 1"
  },
  "15-18h": {
    "temp": "10°",
    "ressenti": "6°",
    "vent": "17-36",
    "direction": "SO",
    "pluie_percent": "25%",
    "pluie_mm": "-"
  },
  "18-21h": {
    "temp": "8°",
    "ressenti": "4°",
    "vent": "14-27",
    "direction": "SSO",
    "pluie_percent": "10%",
    "pluie_mm": "-"
  },
  "21-24h": {
    "temp": "7°",
    "ressenti": "3°",
    "vent": "12-26",
    "direction": "S",
    "pluie_percent": "0%",
    "pluie_mm": "-"
  }
}

Voici le code mis à jour:

// ────────────────────────────────────────────────
// 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 <= 0) return "#3828c7";
    if ($s <= 5) return "#6FA8FF";
    if ($s <= 15) return "#b3e146";
    if ($s <= 20) return "#3CFF3C";
    if ($s <= 28) 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 "#a562a5";
    if ($p <= 2) return "#FFD700";
    if ($p <= 5) return "#FF8C00";
    return "#FF0000";
}

function pluie_percent($value) {
    preg_match_all('/\d+\.?\d*/', $value, $m);
    $nums = array_map('floatval', $m[0]);
    $p = $nums ? max($nums) : 0;
    if ($p == 0) return "#a562a5";
    if ($p <= 20) return "#FFD700";
    if ($p <= 50) 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" => "\u{1F4A8}",
    "Direction" => "\u{1F9ED}",     
    "Tmax" => "\u{2B06}\u{FE0F}",  
    "Tmin" => "\u{2B07}\u{FE0F}",   
    "Pluie" => "\u{1F327}\u{FE0F}", 
    "Soleil" => "\u{2600}\u{FE0F}" 
];


// ────────────────────────────────────────────────
// 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 $index => $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) : '';

    $data_meteo[$index] = [
        'jour_court' => $short_day,
        'jour_long'  => $long_day,
        'donnees'    => []
    ];    

    $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

        $data_meteo[$col]['donnees'][$label] = $v;

        $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] ?? 'rgb(var(--txt-color))';
        }

        $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++;
}

$json_resultat = json_encode($data_meteo, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

$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);
      $pluie_mm    = $xpath->query(".//tr[contains(@class,'precips')]//td//div[contains(@class,'cell')]//span", $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>'
                     . '<th style="padding:4px 3px;font-size:11px;text-align=center;">(mm)</th>'
                     . '</tr></thead><tbody>';

      $slots = min(8, $time_cells->length);

      $data_meteo_detail = [];

      for ($i = 0; $i < $slots; $i++) {
          $h_node = $xpath->query(".//time", $time_cells->item($i))->item(0);
          $h = $h_node ? trim($h_node->textContent) : '?';

          if ($h === '0300') {
            $h = '0-3h';
          } elseif ($h === '0600') {
            $h = '3-6h';
          } elseif ($h === '0900') {
            $h = '6-9h';
          } elseif ($h === '1200') {
            $h = '9-12h';
          } elseif ($h === '1500') {
            $h = '12-15h';
          } elseif ($h === '1800') {
            $h = '15-18h';
          } elseif ($h === '2100') {
            $h = '18-21h';
          } elseif ($h === '2400') {
            $h = '21-24h';
          }

          $data_meteo_detail[$h] = [
              'temp' => trim($temp_cells->item($i)->textContent ?? '-'),
              'ressenti' => trim($feel_cells->item($i)->textContent ?? '-'),
              'vent' => trim($wind_cells->item($i)->textContent ?? '-'),
              'direction' => ($dir_cells->item($i) ? trim($dir_cells->item($i)->textContent) : '-'),
              'pluie_percent' => trim($pluie_cells->item($i)->textContent ?? '0%'),
              'pluie_mm' => trim($pluie_mm->item($i)->textContent ?? '0 mm')
          ];

          $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%');
          $pluie_extract_mm  = trim($pluie_mm->item($i)->textContent ?? '0 mm');

          $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) : 'rgb(var(--txt-color))';

          $temp_color = ($temp !== '-') ? temp_color($temp) : 'rgb(var(--txt-color))';

          $temp_color_ressenti = ($ressenti !== '-') ? temp_color($ressenti) : 'rgb(var(--txt-color))';

          $pluie_color = pluie_percent($pluie);
          $pluie_mm_color = pluie_bg($pluie_extract_mm);

          $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;color:' . $pluie_color . '">' . htmlspecialchars($pluie) . '</td>'
                         . '<td style="padding:4px 3px;margin:0;color:' . $pluie_mm_color . '">' . htmlspecialchars($pluie_extract_mm) . '</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();

    $cmdJson = new cmd();
    $cmdJson->setEqLogic_id($virtual->getId());
    $cmdJson->setLogicalId('data_meteo_json');
    $cmdJson->setName('Données Météo JSON');
    $cmdJson->setType('info');
    $cmdJson->setSubType('string');
    $cmdJson->setIsVisible(0);
    $cmdJson->save();
} else {
    $scenario->setLog("Équipement virtuel trouvé : " . $virtual->getName());
    $cmdMain = $virtual->getCmd('info', 'tableau_meteo');
    $cmdJson = $virtual->getCmd('info', 'data_meteo_json');
    if (!$cmdMain) {
        $scenario->setLog("ATTENTION : commande 'tableau_meteo' manquante → création");
        $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();
    }
    if (!$cmdJson) {
        $scenario->setLog("ATTENTION : commande 'data_meteo_json' manquante → création");
        $cmdJson = new cmd();
        $cmdJson->setEqLogic_id($virtual->getId());
        $cmdJson->setLogicalId('data_meteo_json');
        $cmdJson->setName('Données Météo JSON');
        $cmdJson->setType('info');
        $cmdJson->setSubType('string');
        $cmdJson->setIsVisible(0);
        $cmdJson->save();
    }
}

$cmdMain = $virtual->getCmd('info', 'tableau_meteo');
if ($cmdMain) {
    $virtual->checkAndUpdateCmd('tableau_meteo', $table);
    $virtual->refreshWidget();
    $scenario->setLog("Principal mis à jour");
}

$cmdJson = $virtual->getCmd('info', 'data_meteo_json');
if ($cmdJson) {
    $virtual->checkAndUpdateCmd('data_meteo_json', $json_resultat);
    $scenario->setLog("JSON 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();

        $cmdJsonT = new cmd();
        $cmdJsonT->setEqLogic_id($virtualToday->getId());
        $cmdJsonT->setLogicalId('data_meteo_json_today');
        $cmdJsonT->setName('Données Météo JSON Aujourd\'hui');
        $cmdJsonT->setType('info');
        $cmdJsonT->setSubType('string');
        $cmdJsonT->setIsVisible(0);
        $cmdJsonT->save();
  } else {
        $scenario->setLog("Équipement virtuel pour today trouvé : " . $virtualToday->getName());
        $cmdT = $virtualToday->getCmd('info', 'tableau_today');
        $cmdJsonT = $virtualToday->getCmd('info', 'data_meteo_json_today');
        if (!$cmdT) {
            $scenario->setLog("ATTENTION : commande 'tableau_today' manquante → création");
            $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();
        }
        if (!$cmdJsonT) {
            $scenario->setLog("ATTENTION : commande 'data_meteo_json_today' manquante → création");
            $cmdJsonT = new cmd();
            $cmdJsonT->setEqLogic_id($virtualToday->getId());
            $cmdJsonT->setLogicalId('data_meteo_json_today');
            $cmdJsonT->setName('Données Météo JSON Aujourd\'hui');
            $cmdJsonT->setType('info');
            $cmdJsonT->setSubType('string');
            $cmdJsonT->setIsVisible(0);
            $cmdJsonT->save();
        }
  }

  $cmdToday = $virtualToday->getCmd('info', 'tableau_today');
  if ($cmdToday) {
      $virtualToday->checkAndUpdateCmd('tableau_today', $today_content);
      $virtualToday->refreshWidget();
      $scenario->setLog("Today mis à jour");
  }
  $cmdJsonToday = $virtualToday->getCmd('info', 'data_meteo_json_today');
  if ($cmdJsonToday) {
      $virtualToday->checkAndUpdateCmd('data_meteo_json_today', json_encode($data_meteo_detail, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
      $scenario->setLog("JSON Today mis à jour");
  }
}

$scenario->setLog("└── Fin script");
3 « J'aime »

Hello,
Dans le dernier code :slight_smile:
Suggestion dans une prochaine version mettre en paramètres tag avant le code l’Url et les valeurs si on désire heures détaillées etc …
Pour éviter d’aller coller dans le code et les fausses manip éventuelles.
En tout cas bravo, dés qu’un vrai dev s’en mêle ça progresse. Même si on sort de l’idée d’origine d’Olive effectivement.
Bien cordialement

Ah en effet, je code sous vscode et si tu ne mets pas le <?php il n’y a plus de coloration syntaxique et je l’enlève ensuite pour le coller dans le bloc code. Là j’ai oublié… Je corrige merci :wink:

Merci mais ce qui est important à mon avis c’est l’idée de départ sans ça on ne fait rien :wink:

Merci @iPapy pour avoir déplacé mon intervention :+1:

1 « J'aime »

Bonjour, bravo pour le code

j’ai juste un petit soucis, a chaque fois que je lance le scenario, cela me fait un nouveau virtuel a chaque lancement

je fais surement quelques chose de pas bon
la seul chose que je change dans le code c’est le lien de ma ville

image

merci

ça doit être à cause de l’accent.

Je te propose rapidement un correctif:

remplace

// ────────────────────────────────────────────────
// 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

par

// ────────────────────────────────────────────────
// 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;
}

$nomVilleSansAccent = iconv('UTF-8', 'ASCII//TRANSLIT', $nomVille);

$nomVilleSansAccent = preg_replace('/[^a-zA-Z0-9 ]+/', '', $nomVilleSansAccent);

// paramètres pour le plugin virtual
$nomVilleAffiche = ucwords(str_replace('-', ' ', $nomVilleSansAccent));   // si tiret remplace par un espace + majuscule, par exemple: saint-avertin → Saint Avertin

merci pour ton aide, plus de creation de virtuel

------------------------------------
[2026-02-07 17:50:03][SCENARIO] -- Début : . Tags : {"#trigger#":"user","#trigger_name#":"","#trigger_id#":"","#trigger_message#":"Scénario lancé manuellement","#trigger_value#":"admin"}
[2026-02-07 17:50:03][SCENARIO] - Exécution du sous-élément de type [action] : code
[2026-02-07 17:50:03][SCENARIO] Exécution d'un bloc code
[2026-02-07 17:50:03][SCENARIO] URL analysée → ville : cl%c3%a9guer | code : 3024682
[2026-02-07 17:50:03][SCENARIO] ┌── Début script Météo cl%c3%a9guer ── (7 jours demandés)
[2026-02-07 17:50:03][SCENARIO] → URL : https://www.meteoblue.com/fr/meteo/semaine/cl%c3%a9guer_france_3024682
[2026-02-07 17:50:03][SCENARIO] Récupération page meteoblue...
[2026-02-07 17:50:03][SCENARIO] Code HTTP : 200 | Taille HTML : 304880 octets
[2026-02-07 17:50:03][SCENARIO] Parsing HTML...
[2026-02-07 17:50:03][SCENARIO] Jours disponibles sur meteoblue : 14
[2026-02-07 17:50:03][SCENARIO] Jours retenus (limite demandée) : 7
[2026-02-07 17:50:03][SCENARIO] Données extraites pour 7 jours
[2026-02-07 17:50:03][SCENARIO] Exemple jour 0 → Tmax: 13 °C | Pluie: 5-10 mm
[2026-02-07 17:50:03][SCENARIO] Tableau HTML généré (11476 caractères)
[2026-02-07 17:50:03][SCENARIO] Tableau today complet reconstruit (8 créneaux)
[2026-02-07 17:50:03][SCENARIO] Le nom de l'équipement ne peut pas être vide : eqLogic Object
(
[id:protected] =>
[name:protected] =>
[logicalId:protected] => meteo_cl%c3%a9guer
[generic_type:protected] =>
[object_id:protected] =>
[eqType_name:protected] => virtual
[isVisible:protected] => 1
[isEnable:protected] => 1
[configuration:protected] =>
[timeout:protected] => 0
[category:protected] =>
[display:protected] =>
[order:protected] => 9999
[comment:protected] =>
[tags:protected] =>
[_debug:protected] =>
[_object:protected] =>
[_needRefreshWidget:protected] => 1
[_timeoutUpdated:protected] =>
[_batteryUpdated:protected] =>
[_changed:protected] => 1
[_cmds:protected] => Array
(
)
)
[2026-02-07 17:50:04][SCENARIO] Fin correcte du scénario

Tu n’aurais pas supprimé cette ligne ?

$nomAffiche = 'météo ' . $nomVilleAffiche;

si c’est la cas alors la remettre après

$nomVilleAffiche = ucwords(str_replace('-', ' ', $nomVilleSansAccent));

effectivement j’avais supprime cette ligne

tout est bon desormais plus de creation multiple des virtuels
image

merci

1 « J'aime »

Par contre le nom n’est pas terrible. Je regarde ça après le match :wink:

ok pas de soucis

tu parles de lens - rennes ou brest - lorient

Je suis plus rugby, tournoi des 6 nations. Angleterre - Pays de Galles

1 « J'aime »

hello,
c’est super ce code ; bien joué :index_pointing_at_the_viewer: @Noyax37 :wave: :+1:

mais c’est quand même malheureux ; maintenant que j’y ai pris gout ; je ne peux pas laisser un code tel quel et que je demande toujours à l’autre d’y apporter sa p’tite touche

1 « J'aime »