Backup automatico script del 2026-07-26 07:00

This commit is contained in:
daniele committed 2026-07-26 07:00:01 +02:00
1 parent 4802b021fe
commit a9bbb92090
19 files changed
+1086 -550

No files matched your search

+260 -148
View File
@@ -342,6 +342,38 @@ def _median_or_single(values):
return median(nums)
def _merge_weathercode(values, preferred=None):
"""
Unisce codici WMO categorici con moda (mai mediana: 61+71 → 66 falso gelicidio).
In pareggio preferisce `preferred` se presente tra i valori, altrimenti il primo.
"""
ints = []
for v in values:
if v is None:
continue
try:
ints.append(int(v))
except (TypeError, ValueError):
pass
if not ints:
return None
if preferred is not None:
try:
preferred = int(preferred)
except (TypeError, ValueError):
preferred = None
counts = {}
for c in ints:
counts[c] = counts.get(c, 0) + 1
max_count = max(counts.values())
modes = [c for c, n in counts.items() if n == max_count]
if len(modes) == 1:
return modes[0]
if preferred is not None and preferred in modes:
return preferred
return ints[0]
# Chiavi solo ICON Italia (precip 02d: niente mediana con AROME HD a San Marino)
HOURLY_KEYS_ICON_ONLY = [
"snow_depth", "showers", "precipitation", "rain", "snowfall",
@@ -349,6 +381,7 @@ HOURLY_KEYS_ICON_ONLY = [
DAILY_KEYS_ICON_ONLY = [
"showers_sum", "precipitation_sum", "rain_sum", "snowfall_sum", "precipitation_hours",
]
PREFERRED_WEATHERCODE_MODEL = "italia_meteo_arpae_icon_2i"
def _merge_hourly_median(hourly_by_model, single_source_keys=None, single_source_model=None):
@@ -397,17 +430,26 @@ def _merge_hourly_median(hourly_by_model, single_source_keys=None, single_source
out[key].append(val)
else:
vals = []
preferred_wc = None
for _m, h in hourly_by_model:
times = h.get("time", []) or []
arr = h.get(key, []) or []
for i, t in enumerate(times):
if _normalize_time_key(str(t)) == ref_k and i < len(arr) and arr[i] is not None:
try:
vals.append(float(arr[i]))
if key == "weathercode":
vals.append(int(arr[i]))
if _m == PREFERRED_WEATHERCODE_MODEL:
preferred_wc = int(arr[i])
else:
vals.append(float(arr[i]))
except (TypeError, ValueError):
pass
break
out[key].append(_median_or_single(vals) if vals else None)
if key == "weathercode":
out[key].append(_merge_weathercode(vals, preferred=preferred_wc) if vals else None)
else:
out[key].append(_median_or_single(vals) if vals else None)
n = len(out["time"])
if n > 1:
order = sorted(range(n), key=lambda i: str(out["time"][i]))
@@ -461,17 +503,26 @@ def _merge_daily_median(daily_by_model, single_source_keys=None, single_source_m
out[key].append(val)
else:
vals = []
preferred_wc = None
for _m, d in daily_by_model:
times = d.get("time", []) or []
arr = d.get(key, []) or []
for i, t in enumerate(times):
if str(t)[:10] == date_str and i < len(arr) and arr[i] is not None:
try:
vals.append(float(arr[i]))
if key == "weathercode":
vals.append(int(arr[i]))
if _m == PREFERRED_WEATHERCODE_MODEL:
preferred_wc = int(arr[i])
else:
vals.append(float(arr[i]))
except (TypeError, ValueError):
pass
break
out[key].append(_median_or_single(vals) if vals else None)
if key == "weathercode":
out[key].append(_merge_weathercode(vals, preferred=preferred_wc) if vals else None)
else:
out[key].append(_median_or_single(vals) if vals else None)
# Ordina cronologicamente (evita buchi nel report se l'unione non era ordinata)
n = len(out["time"])
if n > 1:
@@ -675,7 +726,7 @@ def format_day_label(day_index: int, daily_time_list, with_relative: bool = True
return f"giorno {day_index + 1}"
def analyze_temperature_trend(daily_temps_max, daily_temps_min, days=10):
"""Analizza trend temperatura per identificare fronti caldi/freddi con dettaglio completo"""
"""Analizza trend di Tmax e Tmin (non la media giornaliera) per identificare cali/rialzi."""
if not daily_temps_max or not daily_temps_min:
return None
@@ -683,93 +734,105 @@ def analyze_temperature_trend(daily_temps_max, daily_temps_min, days=10):
if max_days < 3:
return None
# Filtra valori None e calcola temperature medie giornaliere
avg_temps = []
valid_indices = []
tmax_series = []
tmin_series = []
for i in range(max_days):
t_max = daily_temps_max[i]
t_min = daily_temps_min[i]
if t_max is not None and t_min is not None:
avg_temps.append((float(t_max) + float(t_min)) / 2)
valid_indices.append(i)
tmax_series.append(float(t_max))
tmin_series.append(float(t_min))
else:
avg_temps.append(None)
tmax_series.append(None)
tmin_series.append(None)
if len([t for t in avg_temps if t is not None]) < 3:
valid_max = [t for t in tmax_series if t is not None]
valid_min = [t for t in tmin_series if t is not None]
if len(valid_max) < 3 or len(valid_min) < 3:
return None
# Analizza tendenza generale (prime 3 giorni vs ultimi 3 giorni validi)
valid_temps = [t for t in avg_temps if t is not None]
if len(valid_temps) < 3:
return None
first_max = mean(valid_max[:3])
last_max = mean(valid_max[-3:])
first_min = mean(valid_min[:3])
last_min = mean(valid_min[-3:])
delta_max = last_max - first_max
delta_min = last_min - first_min
# Delta dominante per classificare il tipo (maggiore |Δ|)
if abs(delta_max) >= abs(delta_min):
primary_delta = delta_max
primary_series = "max"
else:
primary_delta = delta_min
primary_series = "min"
first_avg = mean(valid_temps[:3])
last_avg = mean(valid_temps[-3:])
diff = last_avg - first_avg
# Contesto: massime finali ancora elevate → niente retorica "fronte freddo" invernale
warm_context = last_max >= 25.0
trend_type = None
trend_intensity = "moderato"
if diff > 5:
trend_type = "fronte_caldo"
trend_intensity = "forte" if diff > 8 else "moderato"
elif diff > 2:
if primary_delta > 5:
trend_type = "fronte_caldo" if not warm_context or primary_delta > 8 else "riscaldamento"
trend_intensity = "forte" if primary_delta > 8 else "moderato"
elif primary_delta > 2:
trend_type = "riscaldamento"
trend_intensity = "moderato"
elif diff < -5:
trend_type = "fronte_freddo"
trend_intensity = "forte" if diff < -8 else "moderato"
elif diff < -2:
elif primary_delta < -5:
if warm_context:
trend_type = "calo_termico"
else:
trend_type = "fronte_freddo"
trend_intensity = "forte" if primary_delta < -8 else "moderato"
elif primary_delta < -2:
trend_type = "raffreddamento"
trend_intensity = "moderato"
else:
trend_type = "stabile"
# Identifica giorni di cambio significativo
change_days = []
prev_temp = None
for i, temp in enumerate(avg_temps):
if temp is not None:
if prev_temp is not None:
day_diff = temp - prev_temp
if abs(day_diff) > 3: # Cambio significativo (>3°C)
prev_max = prev_min = None
for i in range(max_days):
tm = tmax_series[i]
tn = tmin_series[i]
if tm is not None and tn is not None:
if prev_max is not None and prev_min is not None:
d_max = tm - prev_max
d_min = tn - prev_min
if abs(d_max) > 3:
change_days.append({
"day": i,
"delta": round(day_diff, 1),
"from": round(prev_temp, 1),
"to": round(temp, 1)
"series": "max",
"delta": round(d_max, 1),
"from": round(prev_max, 1),
"to": round(tm, 1),
})
prev_temp = temp
# Analisi per periodi (primi 3 giorni, medio termine, lungo termine)
period_analysis = {}
if len(valid_temps) >= 7:
period_analysis["short_term"] = {
"avg": round(mean(valid_temps[:3]), 1),
"range": round(max(valid_temps[:3]) - min(valid_temps[:3]), 1)
}
mid_start = len(valid_temps) // 3
mid_end = (len(valid_temps) * 2) // 3
period_analysis["mid_term"] = {
"avg": round(mean(valid_temps[mid_start:mid_end]), 1),
"range": round(max(valid_temps[mid_start:mid_end]) - min(valid_temps[mid_start:mid_end]), 1)
}
period_analysis["long_term"] = {
"avg": round(mean(valid_temps[-3:]), 1),
"range": round(max(valid_temps[-3:]) - min(valid_temps[-3:]), 1)
}
if abs(d_min) > 3:
change_days.append({
"day": i,
"series": "min",
"delta": round(d_min, 1),
"from": round(prev_min, 1),
"to": round(tn, 1),
})
prev_max, prev_min = tm, tn
return {
"type": trend_type,
"intensity": trend_intensity,
"delta": round(diff, 1),
"delta": round(primary_delta, 1),
"delta_max": round(delta_max, 1),
"delta_min": round(delta_min, 1),
"primary_series": primary_series,
"warm_context": warm_context,
"change_days": change_days,
"first_avg": round(first_avg, 1),
"last_avg": round(last_avg, 1),
"period_analysis": period_analysis,
"daily_avg_temps": avg_temps,
"first_max": round(first_max, 1),
"last_max": round(last_max, 1),
"first_min": round(first_min, 1),
"last_min": round(last_min, 1),
# Compatibilità chiavi legacy (usate da generate_practical_advice)
"first_avg": round((first_max + first_min) / 2, 1),
"last_avg": round((last_max + last_min) / 2, 1),
"daily_max": daily_temps_max[:max_days],
"daily_min": daily_temps_min[:max_days]
"daily_min": daily_temps_min[:max_days],
}
def analyze_weather_transitions(daily_weathercodes):
@@ -807,13 +870,18 @@ def analyze_weather_transitions(daily_weathercodes):
return transitions
def get_precip_type(code):
"""Definisce il tipo di precipitazione in base al codice WMO."""
if (71 <= code <= 77) or code in [85, 86]:
def get_precip_type(code, temp=None):
"""Definisce il tipo di precipitazione in base al codice WMO (gate termico per neve/gelicidio)."""
try:
code = int(code) if code is not None else 0
except (TypeError, ValueError):
code = 0
cold_enough = temp is None or float(temp) <= 1.0
if ((71 <= code <= 77) or code in [85, 86]) and cold_enough:
return "❄️ Neve"
if code in [96, 99]:
return "⚡🌨 Grandine"
if code in [66, 67]:
if code in [66, 67] and cold_enough:
return "🧊☔ Pioggia Congelantesi"
return "☔ Pioggia"
@@ -824,6 +892,40 @@ def get_intensity_label(mm_h):
return "Moderata"
return "Forte ⚠️"
def _add_one_hour_hhmm(hhmm):
"""Somma 1 ora a una stringa HH:MM (mod 24)."""
try:
h, m = hhmm.split(":")
return f"{(int(h) + 1) % 24:02d}:{int(m):02d}"
except (ValueError, AttributeError):
return hhmm
def _format_event_hours(times, start_idx, end_idx_inclusive):
"""
Formato HH:MM-HH:MM per bucket orari Open-Meteo.
Una sola ora → 20:00-21:00 (mai 20:00-20:00). Fine = ora successiva all'ultima inclusa.
"""
if not times or start_idx < 0 or start_idx >= len(times):
return "??:??-??:??"
end_idx_inclusive = max(start_idx, min(end_idx_inclusive, len(times) - 1))
start_time = times[start_idx].split("T")[1][:5] if "T" in str(times[start_idx]) else str(times[start_idx])[:5]
next_idx = end_idx_inclusive + 1
if next_idx < len(times):
end_time = times[next_idx].split("T")[1][:5] if "T" in str(times[next_idx]) else str(times[next_idx])[:5]
else:
last_t = times[end_idx_inclusive].split("T")[1][:5] if "T" in str(times[end_idx_inclusive]) else str(times[end_idx_inclusive])[:5]
end_time = _add_one_hour_hhmm(last_t)
if start_time == end_time:
end_time = _add_one_hour_hhmm(start_time)
return f"{start_time}-{end_time}"
ICE_EVENT_MAX_AIR_TEMP = 2.0 # scarta pericoli invernali se Tmin blocco > questa soglia
GELICIDIO_MAX_AIR_TEMP = 1.0
def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, snowfalls=None, rains=None, soil_temps=None, cloud_covers=None, wind_speeds=None):
"""Scansiona le 24 ore e trova blocchi di eventi continui."""
events = []
@@ -838,14 +940,13 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
if cloud_covers is None:
cloud_covers = [None] * len(times)
if wind_speeds is None:
wind_speeds = [None] * len(times)
wind_speeds = winds if winds else [None] * len(times)
# Calcola precipitazioni cumulative delle 3h precedenti per ogni punto
# Calcola precipitazioni cumulate nelle 3h precedenti per ogni ora
precip_3h_sum = []
rain_3h_sum = []
snow_3h_sum = []
for i in range(len(times)):
# Somma delle 3 ore precedenti (i-3, i-2, i-1)
start_idx = max(0, i - 3)
precip_sum = sum([float(p) if p is not None else 0.0 for p in precip[start_idx:i]])
rain_sum = sum([float(r) if r is not None else 0.0 for r in rains[start_idx:i]])
@@ -854,7 +955,7 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
rain_3h_sum.append(rain_sum)
snow_3h_sum.append(snow_sum)
# 1. PERICOLI (Ghiaccio, Gelo, Brina) - Logica migliorata allineata a check_ghiaccio.py
# 1. PERICOLI (Ghiaccio, Gelo, Brina) - con gate termici anti falsi positivi estivi
in_ice = False
start_ice = 0
ice_type = ""
@@ -883,7 +984,7 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
try:
hour = int(times[i].split("T")[1].split(":")[0]) if "T" in times[i] else 12
is_night = (hour >= 18) or (hour <= 6)
except:
except Exception:
is_night = False
# Calcola temperatura suolo: usa valore misurato se disponibile, altrimenti stima (1-2°C più fredda)
@@ -891,48 +992,41 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
t_soil = t - 1.5 # Approssimazione conservativa
# Applica raffreddamento radiativo: cielo sereno + notte + vento debole
# Riduce la temperatura del suolo di 0.5-1.5°C (come in check_ghiaccio.py)
t_soil_adjusted = t_soil
if is_night and cloud is not None and cloud < 20.0:
if wind is None or wind < 5.0:
cooling = 1.5 # Vento molto debole = più raffreddamento
cooling = 1.5
elif wind < 10.0:
cooling = 1.0
else:
cooling = 0.5
t_soil_adjusted = t_soil - cooling
# Precipitazioni nelle 3h precedenti
p_3h = precip_3h_sum[i] if i < len(precip_3h_sum) else 0.0
r_3h = rain_3h_sum[i] if i < len(rain_3h_sum) else 0.0
s_3h = snow_3h_sum[i] if i < len(snow_3h_sum) else 0.0
# LOGICA MIGLIORATA (allineata a check_ghiaccio.py):
current_ice_condition = None
# 1. GELICIDIO (Freezing Rain) - priorità massima
# 1. GELICIDIO: codice 66/67 richiede anche T aria <= soglia (evita WMO spurii in estate)
is_raining_code = (50 <= c <= 69) or (80 <= c <= 82)
if c in [66, 67] or (p > 0 and t <= 0 and is_raining_code):
if (c in [66, 67] and t <= GELICIDIO_MAX_AIR_TEMP) or (p > 0 and t <= 0 and is_raining_code):
current_ice_condition = "🧊☠️ GELICIDIO"
# 2. Black Ice o Neve Ghiacciata - Precipitazione nelle 3h precedenti + suolo gelato
elif p_3h > 0.1 and t_soil_adjusted < 0.0:
# Distingue tra neve e pioggia
# 2. Black Ice o Neve Ghiacciata
elif p_3h > 0.1 and t_soil_adjusted < 0.0 and t <= ICE_EVENT_MAX_AIR_TEMP:
has_snow = (s_3h > 0.1) or (snowfall_curr > 0.1)
has_rain = (r_3h > 0.1) or (rain_curr > 0.1)
if has_snow:
current_ice_condition = "⛸️⚠️ Neve ghiacciata (suolo gelato)"
elif has_rain:
current_ice_condition = "⛸️⚠️ Black Ice (strada bagnata + suolo gelato)"
else:
current_ice_condition = "⛸️⚠️ Black Ice (strada bagnata + suolo gelato)"
# 3. BRINA (Hoar Frost) - Suolo <= 0°C e punto di rugiada > suolo ma < 0°C
elif p_3h <= 0.1 and t_soil_adjusted <= 0.0 and d is not None:
# 3. BRINA
elif p_3h <= 0.1 and t_soil_adjusted <= 0.0 and d is not None and t <= ICE_EVENT_MAX_AIR_TEMP:
if d > t_soil_adjusted and d < 0.0:
current_ice_condition = "⛸️⚠️ GHIACCIO/BRINA"
# 4. GELATA - Temperatura aria < 0°C (senza altre condizioni)
# 4. GELATA
elif t < 0:
current_ice_condition = "🧊 Gelata"
@@ -941,23 +1035,22 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
start_ice = i
ice_type = current_ice_condition
elif (not current_ice_condition and in_ice) or (in_ice and current_ice_condition != ice_type) or (in_ice and i == len(times)-1):
end_idx = i if not current_ice_condition else i
if end_idx > start_ice:
start_time = times[start_ice].split("T")[1][:5]
end_time = times[min(end_idx, len(times)-1)].split("T")[1][:5]
temp_block = temps[start_ice:min(end_idx+1, len(temps))]
temp_block_clean = [t for t in temp_block if t is not None]
if not current_ice_condition and in_ice:
end_inclusive = i - 1
elif in_ice and current_ice_condition and current_ice_condition != ice_type:
end_inclusive = i - 1
else:
end_inclusive = i
if end_inclusive >= start_ice:
hours_str = _format_event_hours(times, start_ice, end_inclusive)
temp_block = temps[start_ice:end_inclusive + 1]
temp_block_clean = [tv for tv in temp_block if tv is not None]
min_t = min(temp_block_clean) if temp_block_clean else 0
# Per GHIACCIO/BRINA, verifica che la temperatura minima sia effettivamente sotto/sopra soglia critica
# Se la temperatura minima è > 1.5°C, non è un rischio reale
if ice_type == "⛸️⚠️ GHIACCIO/BRINA" and min_t > 1.5:
# Non segnalare se la temperatura minima è troppo alta
pass
else:
events.append(f"{ice_type}: {start_time}-{end_time} (Min: {min_t:.0f}°C)")
# Gate termico su tutti i pericoli invernali (non solo brina)
if min_t <= ICE_EVENT_MAX_AIR_TEMP:
events.append(f"{ice_type}: {hours_str} (Min: {min_t:.0f}°C)")
in_ice = False
if current_ice_condition:
if current_ice_condition:
in_ice = True
start_ice = i
ice_type = current_ice_condition
@@ -975,48 +1068,53 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
in_rain = True
start_idx = i
code_val = codes[i] if i < len(codes) and codes[i] is not None else 0
t_val = temps[i] if i < len(temps) and temps[i] is not None else None
try:
code_val = int(code_val) if code_val is not None else 0
except (ValueError, TypeError):
code_val = 0
current_rain_type = get_precip_type(code_val)
current_rain_type = get_precip_type(code_val, temp=t_val)
elif in_rain and is_raining and i < len(codes):
code_val = codes[i] if codes[i] is not None else 0
t_val = temps[i] if i < len(temps) and temps[i] is not None else None
try:
code_val = int(code_val) if code_val is not None else 0
except (ValueError, TypeError):
code_val = 0
new_type = get_precip_type(code_val)
new_type = get_precip_type(code_val, temp=t_val)
if new_type != current_rain_type:
end_idx = i
block_precip = precip[start_idx:end_idx] if end_idx <= len(precip) else precip[start_idx:]
end_inclusive = i - 1
block_precip = precip[start_idx:i] if i <= len(precip) else precip[start_idx:]
block_precip_clean = [p for p in block_precip if p is not None]
tot_mm = sum(block_precip_clean)
start_time = times[start_idx].split("T")[1][:5]
end_time = times[end_idx].split("T")[1][:5] if end_idx < len(times) else times[-1].split("T")[1][:5]
hours_str = _format_event_hours(times, start_idx, end_inclusive)
avg_intensity = tot_mm / len(block_precip) if block_precip else 0
events.append(
f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n"
f" 🕒 {start_time}-{end_time} | 💧 {tot_mm:.1f}mm"
f" 🕒 {hours_str} | 💧 {tot_mm:.1f}mm"
)
start_idx = i
current_rain_type = new_type
elif (not is_raining and in_rain) or (in_rain and i == len(times)-1):
in_rain = False
end_idx = i if not is_raining else i + 1
block_precip = precip[start_idx:end_idx] if end_idx <= len(precip) else precip[start_idx:]
if not is_raining:
end_inclusive = i - 1
end_slice = i
else:
end_inclusive = i
end_slice = i + 1
block_precip = precip[start_idx:end_slice] if end_slice <= len(precip) else precip[start_idx:]
block_precip_clean = [p for p in block_precip if p is not None]
tot_mm = sum(block_precip_clean)
if tot_mm > 0:
start_time = times[start_idx].split("T")[1][:5]
end_time = times[min(end_idx-1, len(times)-1)].split("T")[1][:5]
if tot_mm > 0 and end_inclusive >= start_idx:
hours_str = _format_event_hours(times, start_idx, end_inclusive)
avg_intensity = tot_mm / len(block_precip) if block_precip else 0
events.append(
f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n"
f" 🕒 {start_time}-{end_time} | 💧 {tot_mm:.1f}mm"
f" 🕒 {hours_str} | 💧 {tot_mm:.1f}mm"
)
# 3. VENTO
@@ -1027,10 +1125,10 @@ def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints, s
if max_wind > SOGLIA_VENTO_KMH:
try:
peak_idx = winds.index(max_wind)
except ValueError:
peak_idx = 0
peak_time = times[min(peak_idx, len(times)-1)].split("T")[1][:5]
events.append(f"💨 Vento Forte: Picco {max_wind:.0f}km/h alle {peak_time}")
peak_time = times[peak_idx].split("T")[1][:5]
events.append(f"💨 Picco vento: {max_wind:.0f}km/h alle {peak_time}")
except (ValueError, IndexError, AttributeError):
events.append(f"💨 Picco vento: {max_wind:.0f}km/h")
return events
@@ -1042,6 +1140,8 @@ def generate_practical_advice(trend, transitions, events_summary, daily_data):
if trend:
if trend["type"] == "fronte_freddo" and trend["intensity"] == "forte":
advice.append("❄️ <b>Fronte Freddo in Arrivo:</b> Preparati a temperature in calo significativo. Controlla riscaldamento, proteggi piante sensibili.")
elif trend["type"] == "calo_termico" and trend["intensity"] == "forte":
advice.append("📉 <b>Calo termico:</b> Massime e/o minime in netto ribasso rispetto all'inizio periodo, senza necessariamente gelo.")
elif trend["type"] == "fronte_caldo" and trend["intensity"] == "forte":
advice.append("🔥 <b>Ondata di Calore:</b> Temperature in aumento. Mantieni case fresche, idratazione importante, attenzione a persone fragili.")
elif trend["type"] == "raffreddamento":
@@ -1075,55 +1175,58 @@ def generate_practical_advice(trend, transitions, events_summary, daily_data):
return advice
def format_detailed_trend_explanation(trend, daily_time_list=None, display_days=DISPLAY_FORECAST_DAYS):
"""Genera spiegazione dettagliata del trend temperatura sui giorni in previsione."""
"""Genera spiegazione dettagliata del trend su Tmax e Tmin (non temperature medie)."""
if not trend:
return ""
explanation = []
explanation.append(f"📊 <b>EVOLUZIONE TEMPERATURE ({display_days} GIORNI)</b>\n")
# Trend principale con spiegazione chiara
trend_type = trend["type"]
intensity = trend["intensity"]
delta = trend['delta']
first_avg = trend['first_avg']
last_avg = trend['last_avg']
first_max = trend["first_max"]
last_max = trend["last_max"]
first_min = trend["first_min"]
last_min = trend["last_min"]
delta_max = trend["delta_max"]
delta_min = trend["delta_min"]
if trend_type == "fronte_caldo":
trend_desc = "🔥 <b>Fronte Caldo in Arrivo</b>"
desc_text = f"Arrivo di aria più calda: temperatura media passerà da {first_avg:.1f}°C a {last_avg:.1f}°C (+{delta:.1f}°C)."
elif trend_type == "fronte_freddo":
trend_desc = "❄️ <b>Fronte Freddo in Arrivo</b>"
desc_text = f"Arrivo di aria più fredda: temperatura media scenderà da {first_avg:.1f}°C a {last_avg:.1f}°C ({delta:+.1f}°C)."
elif trend_type == "calo_termico":
trend_desc = "📉 <b>Calo Termico</b>"
elif trend_type == "riscaldamento":
trend_desc = "📈 <b>Riscaldamento Progressivo</b>"
desc_text = f"Tendenza al rialzo delle temperature: da {first_avg:.1f}°C a {last_avg:.1f}°C (+{delta:.1f}°C)."
elif trend_type == "raffreddamento":
trend_desc = "📉 <b>Raffreddamento Progressivo</b>"
desc_text = f"Tendenza al ribasso delle temperature: da {first_avg:.1f}°C a {last_avg:.1f}°C ({delta:+.1f}°C)."
elif trend_type == "stabile":
trend_desc = "➡️ <b>Temperature Stabili</b>"
desc_text = f"Temperature medie sostanzialmente stabili: da {first_avg:.1f}°C a {last_avg:.1f}°C (variazione {delta:+.1f}°C)."
else:
trend_desc = "🌡️ <b>Variazione Termica</b>"
desc_text = f"Evoluzione temperature: da {first_avg:.1f}°C a {last_avg:.1f}°C ({delta:+.1f}°C)."
intensity_text = " (variazione significativa)" if intensity == "forte" else " (variazione moderata)"
explanation.append(f"{trend_desc}{intensity_text}")
explanation.append(f"{desc_text}")
explanation.append(
f"Massime: {first_max:.1f}{last_max:.1f}°C ({delta_max:+.1f}°C). "
f"Minime: {first_min:.1f}{last_min:.1f}°C ({delta_min:+.1f}°C)."
)
# Aggiungi solo picchi significativi in modo sintetico (entro i giorni in tabella)
if trend.get("change_days"):
significant_changes = [
c for c in trend["change_days"]
if abs(c["delta"]) > 3.0 and c["day"] < display_days
][:3]
][:4]
if significant_changes:
change_texts = []
for change in significant_changes:
day_name = format_day_label(change["day"], daily_time_list or [])
direction = "" if change['delta'] > 0 else ""
change_texts.append(f"{direction} {day_name}: {change['from']:.0f}°→{change['to']:.0f}°C")
direction = "" if change["delta"] > 0 else ""
series_lbl = "max" if change.get("series") == "max" else "min"
change_texts.append(
f"{direction} {day_name} {series_lbl}: {change['from']:.0f}°→{change['to']:.0f}°C"
)
if change_texts:
explanation.append(f"Picchi: {', '.join(change_texts)}")
@@ -1272,8 +1375,10 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
threshold_mm = 5.0 # Soglia default per pioggia
if precip_amount > 0.1:
# Se snowfall è disponibile e positivo, usa quello (più preciso)
if snow_sum_day > 0.1:
# Se snowfall è disponibile e positivo, usa quello (più preciso) solo con aria fredda
temps_clean_day = [float(t) for t in d_temps_day if t is not None]
day_t_min_early = min(temps_clean_day) if temps_clean_day else None
if snow_sum_day > 0.1 and day_t_min_early is not None and day_t_min_early <= ICE_EVENT_MAX_AIR_TEMP:
# Se c'è neve (anche poca), il simbolo è sempre ❄️ (priorità alla neve)
precip_type_symbol = "❄️" # Neve
threshold_mm = 0.5 # Soglia più bassa per neve (anche pochi mm sono significativi)
@@ -1284,12 +1389,14 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
hail_codes = [96, 99] # Codici WMO per grandine/temporale
snow_count = sum(1 for c in d_codes_day if c is not None and int(c) in snow_codes)
hail_count = sum(1 for c in d_codes_day if c is not None and int(c) in hail_codes)
temps_clean = [float(t) for t in d_temps_day if t is not None]
day_t_min = min(temps_clean) if temps_clean else None
if hail_count > 0:
precip_type_symbol = "⛈️" # Grandine/Temporale
threshold_mm = 5.0
elif snow_count > 0:
# Solo se weathercode indica esplicitamente neve
elif snow_count > 0 and day_t_min is not None and day_t_min <= ICE_EVENT_MAX_AIR_TEMP:
# Weathercode neve solo se aria vicino allo zero (evita falsi ❄️ estivi)
precip_type_symbol = "❄️" # Neve
threshold_mm = 0.5 # Soglia più bassa per neve
@@ -1493,8 +1600,9 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
pass # Gestito separatamente per l'icona meteo
if precip_sum > 0.1:
cold_enough_day = t_min <= ICE_EVENT_MAX_AIR_TEMP
# Priorità 1: Se sta nevicando (snowfall > 0) e c'è manto nevoso, considera entrambi
if has_snow_depth_data and max_snow_depth > 0:
if cold_enough_day and has_snow_depth_data and max_snow_depth > 0:
# C'è sia neve in caduta che manto nevoso persistente
if rain_sum > 0.1 or showers_sum > 0.1:
precip_type = "mixed" # Neve + pioggia/temporali
@@ -1505,7 +1613,7 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
# Il tipo di precipitazione resta quello basato su snowfall/rain
pass
# Priorità 2: Usa dati daily se disponibili
elif snowfall_sum > 0.1:
elif cold_enough_day and snowfall_sum > 0.1:
# C'è neve significativa
if snowfall_sum >= precip_sum * 0.5:
precip_type = "snow"
@@ -1527,7 +1635,7 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
else:
# Fallback: usa dati hourly se daily non disponibili
snow_sum_day = sum([float(s) for s in d_snow if s is not None]) if d_snow else 0.0
if snow_sum_day > 0.1:
if snow_sum_day > 0.1 and t_min <= ICE_EVENT_MAX_AIR_TEMP:
if snow_sum_day >= precip_sum * 0.5:
precip_type = "snow"
else:
@@ -1543,7 +1651,7 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
if hail_count > 0:
precip_type = "hail"
elif snow_count > rain_count:
elif snow_count > rain_count and t_min is not None and float(t_min) <= ICE_EVENT_MAX_AIR_TEMP:
precip_type = "snow"
else:
precip_type = "rain"
@@ -1563,8 +1671,8 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
weather_icon = "🌨️" # Precipitazione mista
else:
weather_icon = "🌧️" # Pioggia
elif has_snow_depth_data and max_snow_depth > 0:
# C'è manto nevoso persistente anche senza precipitazioni
elif has_snow_depth_data and max_snow_depth > 0 and t_min <= ICE_EVENT_MAX_AIR_TEMP:
# C'è manto nevoso persistente anche senza precipitazioni (solo se aria abbastanza fredda)
# Mostra icona neve anche se non sta nevicando
weather_icon = "❄️" # Manto nevoso presente
elif t_min < 0:
@@ -1699,8 +1807,8 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
# Caratterizza usando dati daily se disponibili
precip_parts = []
# Neve
if day_info.get('snowfall_sum', 0) > 0.1:
# Neve (solo con aria abbastanza fredda: evita cm spurii in estate)
if day_info.get('snowfall_sum', 0) > 0.1 and day_info['t_min'] <= ICE_EVENT_MAX_AIR_TEMP:
precip_parts.append(f"❄️ {day_info['snowfall_sum']:.1f}cm")
# Pioggia
@@ -1715,6 +1823,10 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
if not precip_parts:
precip_symbol = "❄️" if day_info['precip_type'] == "snow" else "⛈️" if day_info['precip_type'] in ("hail", "thunderstorms") else "🌨️" if day_info['precip_type'] == "mixed" else "🌧️"
precip_parts.append(f"{precip_symbol} {day_info['precip_sum']:.1f}mm")
elif day_info['precip_sum'] > 0.1 and day_info.get('snowfall_sum', 0) > 0.1 and day_info['t_min'] > ICE_EVENT_MAX_AIR_TEMP:
# snowfall spurio a caldo: assicurati che l'accumulo pioggia totale sia visibile
if not any("🌧️" in p or "⛈️" in p for p in precip_parts):
precip_parts.append(f"🌧️ {day_info['precip_sum']:.1f}mm")
line += f" | {' + '.join(precip_parts)}"
@@ -1738,7 +1850,7 @@ def format_weather_context_report(models_data, location_name, country_code, as_j
elif snow_depth_avg is not None and snow_depth_avg > 0:
snow_depth_end = snow_depth_avg # Usa la media come fallback
if snow_depth_end is not None and snow_depth_end > 0:
if snow_depth_end is not None and snow_depth_end > 0 and day_info['t_min'] <= ICE_EVENT_MAX_AIR_TEMP:
snow_depth_str = f"❄️ Manto nevoso: {snow_depth_end:.1f} cm"
# Mostra evoluzione rispetto al giorno precedente
if prev_snow_depth_end is not None: