Backup automatico script del 2026-07-26 07:00
This commit is contained in:
1 parent
4802b021fe
commit
a9bbb92090
19 files changed
+1086
-550
No files matched your search
@@ -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 0–2d: 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:
|
||||
|
||||
Reference in new issue
Block a user