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

This commit is contained in:
2026-07-05 07:00:03 +02:00
parent bd3b4b66cc
commit b5a2a814e2
3 changed files with 221 additions and 28 deletions
+10 -1
View File
@@ -20,6 +20,12 @@ EXCLUDED_FILES = {
"road_weather.log",
"snow_radar.log",
}
SEASONAL_EXCLUDED_FILES = {
"freeze_alert.log",
"arome_snow_alert.log",
"meteo.log",
}
SEASONAL_EXCLUDED_MONTHS = {5, 6, 7, 8, 9} # maggio-settembre
# Log irrigazione: aggiornato solo quando lo script viene eseguito (cron --auto o /irrigazione).
# Se non c’è un cron giornaliero, il file può restare “non aggiornato” per giorni (normale in inverno).
STALE_EXCLUDE_BASENAMES = {"irrigation_advisor.log"}
@@ -230,7 +236,10 @@ def main():
for pat in DEFAULT_PATTERNS:
files.extend(sorted([str(p) for p in Path(BASE_DIR).glob(pat)]))
files = sorted(set(files))
files = [p for p in files if os.path.basename(p) not in EXCLUDED_FILES]
now_month = datetime.datetime.now().month
seasonal_exclusions = SEASONAL_EXCLUDED_FILES if now_month in SEASONAL_EXCLUDED_MONTHS else set()
excluded_names = EXCLUDED_FILES | seasonal_exclusions
files = [p for p in files if os.path.basename(p) not in excluded_names]
since = datetime.datetime.now() - datetime.timedelta(days=args.days)
category_hits, per_file_counts, timeout_minutes, stale_logs = analyze_logs(files, since, args.max_lines)
+183 -17
View File
@@ -72,6 +72,33 @@ RAIN_INTENSE_THRESHOLD_H = 20.0 # mm/h - Soglia per nubifragio orario
RAIN_INTENSE_THRESHOLD_3H = 40.0 # mm/3h - Soglia per nubifragio su 3 ore
STORM_SCORE_THRESHOLD = 40.0 # Storm Severity Score minimo per allerta
# Pioggia minima per considerare un evento REALMENTE convettivo.
# Il CAPE è solo energia potenziale: senza precipitazione non c'è temporale
# (evita falsi allarmi "fulminazioni" con cielo asciutto).
CONVECTIVE_MIN_PRECIP_H = 0.5 # mm/h
CONVECTIVE_MIN_PRECIP_3H = 2.0 # mm/3h
# ----------------- SOGLIE DI SIGNIFICATIVITÀ (per NOTIFICA temporali) -----------------
# Si notifica solo se c'è almeno un evento "significativo": fulminazioni forti,
# bomba d'acqua o downburst. Soglie alte per filtrare i temporali marginali estivi.
SIGNIFICANT_CAPE_LIGHTNING = 1200.0 # J/kg - fulminazioni rilevanti (instabilità moderata: scelta orientata alla sicurezza)
# La fulminazione "forte" deve corrispondere a una cella temporalesca prevista sul posto:
# oltre a CAPE+LPI richiede precipitazione convettiva consistente (non semplice pioggerella).
SIGNIFICANT_LIGHTNING_PRECIP_H = 2.0 # mm/h
SIGNIFICANT_LIGHTNING_PRECIP_3H = 5.0 # mm/3h
SIGNIFICANT_PRECIP_H = 30.0 # mm/h - bomba d'acqua (nubifragio forte)
SIGNIFICANT_PRECIP_3H = 50.0 # mm/3h - nubifragio forte su 3 ore
SIGNIFICANT_STORM_SCORE = 55.0 # Storm Severity Score minimo per significativo
# ----------------- RATE LIMITING NOTIFICHE TEMPORALI -----------------
# Notifiche temporali "molto contingentate":
# - eventi imminenti (entro IMMINENT_HOURS): massimo 2 al giorno (cooldown 6h)
# - eventi solo nella finestra estesa (24-48h): massimo 1 al giorno
IMMINENT_HOURS = 24
MAX_STORM_ALERTS_IMMINENT_PER_DAY = 2
MAX_STORM_ALERTS_EXTENDED_PER_DAY = 1
MIN_GAP_HOURS_IMMINENT = 6.0
# ----------------- FILES -----------------
STATE_FILE = "/home/daniely/docker/telegram-bot/weather_state.json"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -239,6 +266,12 @@ def load_state() -> Dict:
"last_storm_score": 0.0,
"last_alert_type": None, # Tipo di allerta: "VENTO", "PIOGGIA", "TEMPORALI", o lista combinata
"last_alert_time": None, # Timestamp ISO dell'ultima notifica
# Anti-spam temporali: conteggi giornalieri per finestra + dedup
"storm_daily": {}, # {"YYYY-MM-DD": {"imminent": int, "extended": int}}
"storm_last_sent_imminent": None,
"storm_last_sent_extended": None,
"storm_last_signature": None,
"storm_last_signature_date": None,
}
if os.path.exists(STATE_FILE):
try:
@@ -761,21 +794,27 @@ def analyze_convective_risk(icon_data: Dict, arome_data: Dict, times_base: List[
dynamic_score = min(30.0, ((gusts_val - WIND_GUST_DOWNBURST_THRESHOLD) / 40.0) * 30.0)
score += dynamic_score
# Precipitazione convettiva reale: senza pioggia non c'è temporale.
has_convective_precip = (precip_val >= CONVECTIVE_MIN_PRECIP_H or
precip_3h_val >= CONVECTIVE_MIN_PRECIP_3H)
# Identifica minacce specifiche
# Fulminazioni
if cape_val > CAPE_LIGHTNING_THRESHOLD and lpi_val > 0:
# Fulminazioni: CAPE + LPI MA solo se c'è precipitazione convettiva reale
if cape_val > CAPE_LIGHTNING_THRESHOLD and lpi_val > 0 and has_convective_precip:
threats.append("Fulminazioni")
# Downburst/Temporale violento
if cape_val > CAPE_SEVERE_THRESHOLD and gusts_val > WIND_GUST_DOWNBURST_THRESHOLD:
# Downburst/Temporale violento (richiede comunque precipitazione)
if (cape_val > CAPE_SEVERE_THRESHOLD and gusts_val > WIND_GUST_DOWNBURST_THRESHOLD
and has_convective_precip):
threats.append("Downburst/Temporale violento")
# Nubifragio
if precip_val > RAIN_INTENSE_THRESHOLD_H or precip_3h_val > RAIN_INTENSE_THRESHOLD_3H:
threats.append("Nubifragio")
# Aggiungi risultato solo se supera soglia
if score >= STORM_SCORE_THRESHOLD or threats:
# Aggiungi risultato solo se c'è una minaccia reale (tutte richiedono precipitazione).
# Il CAPE alto da solo (cielo asciutto) NON genera più un evento.
if threats:
results.append({
"timestamp": times_base[i],
"score": score,
@@ -940,6 +979,105 @@ def format_convective_alert(storm_events: List[Dict], times: List[str], start_id
return "\n".join(msg_parts)
# =============================================================================
# SIGNIFICATIVITÀ E ANTI-SPAM (temporali severi)
# =============================================================================
def storm_event_significance(event: Dict) -> List[str]:
"""Ritorna i motivi per cui un evento convettivo è 'significativo' (degno di
notifica). Lista vuota = evento marginale, da NON notificare."""
reasons: List[str] = []
threats = event.get("threats", []) or []
cape = float(event.get("cape", 0.0) or 0.0)
lpi = float(event.get("lpi", 0.0) or 0.0)
precip = float(event.get("precip", 0.0) or 0.0)
precip_3h = float(event.get("precip_3h", 0.0) or 0.0)
score = float(event.get("score", 0.0) or 0.0)
lightning_precip = (precip >= SIGNIFICANT_LIGHTNING_PRECIP_H or
precip_3h >= SIGNIFICANT_LIGHTNING_PRECIP_3H)
if ("Fulminazioni" in threats and cape >= SIGNIFICANT_CAPE_LIGHTNING
and lpi > 0 and lightning_precip):
reasons.append("Fulminazioni forti")
if precip >= SIGNIFICANT_PRECIP_H or precip_3h >= SIGNIFICANT_PRECIP_3H:
reasons.append("Bomba d'acqua")
if "Downburst/Temporale violento" in threats:
reasons.append("Downburst")
if score >= SIGNIFICANT_STORM_SCORE and not reasons:
reasons.append("Temporale severo")
return reasons
def filter_significant_storms(storm_events: List[Dict], now: datetime.datetime) -> List[Dict]:
"""Mantiene solo gli eventi significativi, arricchendoli con 'lead_hours' e
'significance'."""
out: List[Dict] = []
for ev in storm_events or []:
reasons = storm_event_significance(ev)
if not reasons:
continue
try:
lead = (parse_time_to_local(ev["timestamp"]) - now).total_seconds() / 3600.0
except Exception:
lead = 0.0
e2 = dict(ev)
e2["lead_hours"] = lead
e2["significance"] = reasons
out.append(e2)
return out
def _today_key(now: datetime.datetime) -> str:
return now.strftime("%Y-%m-%d")
def _parse_iso_local(s: Optional[str]) -> Optional[datetime.datetime]:
if not s:
return None
try:
dt = parser.isoparse(s)
if dt.tzinfo is None:
return dt.replace(tzinfo=TZINFO)
return dt.astimezone(TZINFO)
except Exception:
return None
def prune_storm_daily(state: Dict, now: datetime.datetime) -> None:
keep = {_today_key(now), _today_key(now - datetime.timedelta(days=1))}
daily = state.get("storm_daily", {}) or {}
state["storm_daily"] = {k: v for k, v in daily.items() if k in keep}
def can_notify_storm(category: str, now: datetime.datetime, state: Dict) -> Tuple[bool, str]:
key = _today_key(now)
daily = state.setdefault("storm_daily", {})
day = daily.setdefault(key, {"imminent": 0, "extended": 0})
if category == "imminent":
if int(day.get("imminent", 0)) >= MAX_STORM_ALERTS_IMMINENT_PER_DAY:
return False, "cap giornaliero imminenti raggiunto"
last = _parse_iso_local(state.get("storm_last_sent_imminent"))
if last and (now - last).total_seconds() < MIN_GAP_HOURS_IMMINENT * 3600:
return False, "cooldown imminenti attivo"
return True, ""
if int(day.get("extended", 0)) >= MAX_STORM_ALERTS_EXTENDED_PER_DAY:
return False, "cap giornaliero estesi raggiunto"
return True, ""
def record_notify_storm(category: str, now: datetime.datetime, state: Dict) -> None:
key = _today_key(now)
day = state.setdefault("storm_daily", {}).setdefault(key, {"imminent": 0, "extended": 0})
day[category] = int(day.get(category, 0)) + 1
state["storm_last_sent_" + category] = now.isoformat()
def build_storm_signature(category: str, sig_events: List[Dict]) -> str:
reasons = set()
for ev in sig_events:
reasons.update(ev.get("significance", []))
return f"{category}|" + "+".join(sorted(reasons))
# =============================================================================
# PERSISTENCE LOGIC
# =============================================================================
@@ -1280,21 +1418,42 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
should_notify = False
# 1) Convective storms (temporali severi) - priorità alta
if storm_events:
prev_storm_active = bool(state.get("convective_storm_active", False))
max_score = max(e["score"] for e in storm_events)
prev_score = float(state.get("last_storm_score", 0.0) or 0.0)
# Considera SOLO eventi significativi (fulminazioni forti / bombe d'acqua /
# downburst) e applica anti-spam a finestre + dedup giornaliero.
sig_storms = filter_significant_storms(storm_events, now)
prune_storm_daily(state, now)
if sig_storms:
has_imminent = any(e.get("lead_hours", 0.0) <= IMMINENT_HOURS for e in sig_storms)
storm_category = "imminent" if has_imminent else "extended"
storm_signature = build_storm_signature(storm_category, sig_storms)
today_key = _today_key(now)
# Notifica se: nuovo evento, o score aumenta significativamente (+15 punti)
if debug_mode or not prev_storm_active or (max_score >= prev_score + 15.0):
send_storm = False
if debug_mode:
LOGGER.info("[DEBUG MODE] Bypass anti-spam: invio forzato per temporali severi")
convective_msg = format_convective_alert(storm_events, times, start_idx)
send_storm = True
elif (state.get("storm_last_signature") == storm_signature
and state.get("storm_last_signature_date") == today_key):
LOGGER.info("Temporali: alert soppresso (dedup): situazione invariata già notificata oggi [%s]", storm_category)
else:
allowed, reason = can_notify_storm(storm_category, now, state)
if allowed:
send_storm = True
else:
LOGGER.info("Temporali: alert soppresso (rate-limit %s): %s", storm_category, reason)
if send_storm:
convective_msg = format_convective_alert(sig_storms, times, start_idx)
if convective_msg:
alerts.append(convective_msg)
should_notify = True
if not debug_mode:
record_notify_storm(storm_category, now, state)
state["storm_last_signature"] = storm_signature
state["storm_last_signature_date"] = today_key
state["convective_storm_active"] = True
state["last_storm_score"] = float(max_score)
state["last_storm_score"] = float(max(e["score"] for e in sig_storms))
else:
state["convective_storm_active"] = False
state["last_storm_score"] = 0.0
@@ -1362,7 +1521,7 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
state["last_rain_3h"] = 0.0
is_alarm_now = (
(storm_events is not None and len(storm_events) > 0)
(len(sig_storms) > 0)
or (wind_level_curr > 0)
or (rain_persist >= PERSIST_HOURS and rain_max_3h >= RAIN_3H_LIMIT)
)
@@ -1383,7 +1542,7 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
model_info = f"{model_used} + ICON Italia (discordanza rilevata)"
# Se ci sono temporali severi, aggiungi informazioni sui modelli usati
if storm_events:
if sig_storms:
model_info = f"{model_used} + ICON Italia (analisi convettiva combinata)"
meta = (
@@ -1407,7 +1566,7 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
if not debug_message_only:
# Determina il tipo di allerta basandosi sulle condizioni attuali
alert_types = []
if storm_events and len(storm_events) > 0:
if sig_storms and len(sig_storms) > 0:
alert_types.append("TEMPORALI SEVERI")
if wind_level_curr > 0:
wind_labels = {3: "TEMPESTA", 2: "VENTO MOLTO FORTE", 1: "VENTO FORTE"}
@@ -1490,6 +1649,13 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
"last_storm_score": 0.0,
"last_alert_type": None,
"last_alert_time": None,
# Preserva i contatori giornalieri/cooldown temporali (i cap valgono
# per l'intera giornata anche dopo un all-clear); azzera solo la firma.
"storm_daily": state.get("storm_daily", {}),
"storm_last_sent_imminent": state.get("storm_last_sent_imminent"),
"storm_last_sent_extended": state.get("storm_last_sent_extended"),
"storm_last_signature": None,
"storm_last_signature_date": None,
}
save_state(state)
return
@@ -76,11 +76,19 @@ STORM_SCORE_THRESHOLD = 40.0 # Storm Severity Score minimo per allerta
# Una località viene notificata SOLO se ha almeno un evento "significativo":
# fulminazioni forti, bomba d'acqua/nubifragio forte, alluvione o downburst.
# Soglie volutamente alte per filtrare i temporali marginali estivi.
SIGNIFICANT_CAPE_LIGHTNING = 1500.0 # J/kg - fulminazioni rilevanti (oltre il marginale)
SIGNIFICANT_CAPE_LIGHTNING = 1200.0 # J/kg - fulminazioni rilevanti (instabilità moderata: scelta orientata alla sicurezza)
# La fulminazione "forte" deve corrispondere a una cella temporalesca prevista sul posto:
# oltre a CAPE+LPI richiede precipitazione convettiva consistente.
SIGNIFICANT_LIGHTNING_PRECIP_H = 2.0 # mm/h
SIGNIFICANT_LIGHTNING_PRECIP_3H = 5.0 # mm/3h
SIGNIFICANT_PRECIP_H = 30.0 # mm/h - bomba d'acqua (nubifragio forte)
SIGNIFICANT_PRECIP_3H = 50.0 # mm/3h - nubifragio forte su 3 ore
SIGNIFICANT_SCORE = 55.0 # Storm Severity Score minimo per considerare l'evento significativo
# Pioggia minima per considerare un evento REALMENTE convettivo (no pioggia = no temporale).
CONVECTIVE_MIN_PRECIP_H = 0.5 # mm/h
CONVECTIVE_MIN_PRECIP_3H = 2.0 # mm/3h
# ----------------- RATE LIMITING NOTIFICHE -----------------
# Notifiche "molto contingentate":
# - eventi imminenti (entro IMMINENT_HOURS): massimo 2 al giorno
@@ -282,7 +290,10 @@ def event_significance(event: Dict) -> List[str]:
precip_3h = float(event.get("precip_3h", 0.0) or 0.0)
score = float(event.get("score", 0.0) or 0.0)
if "Fulminazioni" in threats and cape >= SIGNIFICANT_CAPE_LIGHTNING and lpi > 0:
lightning_precip = (precip >= SIGNIFICANT_LIGHTNING_PRECIP_H or
precip_3h >= SIGNIFICANT_LIGHTNING_PRECIP_3H)
if ("Fulminazioni" in threats and cape >= SIGNIFICANT_CAPE_LIGHTNING
and lpi > 0 and lightning_precip):
reasons.append("Fulminazioni forti")
if precip >= SIGNIFICANT_PRECIP_H or precip_3h >= SIGNIFICANT_PRECIP_3H:
reasons.append("Bomba d'acqua")
@@ -535,11 +546,16 @@ def analyze_convective_risk(icon_data: Dict, arome_data: Dict, times_base: List[
dynamic_score = min(30.0, ((gusts_val - WIND_GUST_DOWNBURST_THRESHOLD) / 40.0) * 30.0)
score += dynamic_score
# Identifica minacce
if cape_val > CAPE_LIGHTNING_THRESHOLD and lpi_val > 0:
# Precipitazione convettiva reale: senza pioggia non c'è temporale.
has_convective_precip = (precip_val >= CONVECTIVE_MIN_PRECIP_H or
precip_3h_val >= CONVECTIVE_MIN_PRECIP_3H)
# Identifica minacce (fulminazioni e downburst richiedono precipitazione reale)
if cape_val > CAPE_LIGHTNING_THRESHOLD and lpi_val > 0 and has_convective_precip:
threats.append("Fulminazioni")
if cape_val > CAPE_SEVERE_THRESHOLD and gusts_val > WIND_GUST_DOWNBURST_THRESHOLD:
if (cape_val > CAPE_SEVERE_THRESHOLD and gusts_val > WIND_GUST_DOWNBURST_THRESHOLD
and has_convective_precip):
threats.append("Downburst/Temporale violento")
if precip_val > RAIN_INTENSE_THRESHOLD_H or precip_3h_val > RAIN_INTENSE_THRESHOLD_3H:
@@ -552,7 +568,9 @@ def analyze_convective_risk(icon_data: Dict, arome_data: Dict, times_base: List[
flood_bonus = min(10.0, (precip_24h_val - RAIN_FLOOD_THRESHOLD_24H) / 10.0)
score += flood_bonus
if score >= STORM_SCORE_THRESHOLD or threats:
# Registra solo eventi con una minaccia reale (tutte richiedono precipitazione).
# Il CAPE alto da solo (cielo asciutto) NON genera più un evento.
if threats:
results.append({
"timestamp": times_base[i],
"score": score,