Backup automatico script del 2026-07-19 07:00
This commit is contained in:
@@ -101,16 +101,56 @@ 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).
|
||||
# Base (fascia "near"): si notifica solo se c'è almeno un evento "significativo".
|
||||
SIGNIFICANT_CAPE_LIGHTNING = 1200.0 # J/kg - fulminazioni rilevanti
|
||||
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
|
||||
SIGNIFICANT_PRECIP_H = 30.0 # mm/h - bomba d'acqua
|
||||
SIGNIFICANT_PRECIP_3H = 50.0 # mm/3h
|
||||
SIGNIFICANT_STORM_SCORE = 55.0
|
||||
|
||||
# ----------------- SOGLIE PER ANTICIPO (lead-time) -----------------
|
||||
# Concetto: avvisa 24h prima solo se è un "finimondo"; eventi moderati bastano
|
||||
# poche ore prima (le previsioni lontane sono spesso fuori scala e poi si smussano).
|
||||
# near: < 6h → soglie base (sensibili)
|
||||
# mid: 6–12h → soglie elevate
|
||||
# far: ≥ 12h → soglie estreme
|
||||
LEAD_NEAR_H = 6.0
|
||||
LEAD_MID_H = 12.0
|
||||
|
||||
LEAD_STORM = {
|
||||
"near": {
|
||||
"cape": SIGNIFICANT_CAPE_LIGHTNING,
|
||||
"lightning_precip_h": SIGNIFICANT_LIGHTNING_PRECIP_H,
|
||||
"lightning_precip_3h": SIGNIFICANT_LIGHTNING_PRECIP_3H,
|
||||
"precip_h": SIGNIFICANT_PRECIP_H,
|
||||
"precip_3h": SIGNIFICANT_PRECIP_3H,
|
||||
"score": SIGNIFICANT_STORM_SCORE,
|
||||
"downburst_gusts": WIND_GUST_DOWNBURST_THRESHOLD,
|
||||
},
|
||||
"mid": {
|
||||
"cape": 1800.0,
|
||||
"lightning_precip_h": 5.0,
|
||||
"lightning_precip_3h": 12.0,
|
||||
"precip_h": 40.0,
|
||||
"precip_3h": 65.0,
|
||||
"score": 70.0,
|
||||
"downburst_gusts": 75.0,
|
||||
},
|
||||
"far": {
|
||||
"cape": 2500.0,
|
||||
"lightning_precip_h": 10.0,
|
||||
"lightning_precip_3h": 25.0,
|
||||
"precip_h": 55.0,
|
||||
"precip_3h": 80.0,
|
||||
"score": 85.0,
|
||||
"downburst_gusts": 90.0,
|
||||
},
|
||||
}
|
||||
# Vento: livello minimo notificato per fascia (1=giallo, 2=arancione, 3=rosso)
|
||||
LEAD_WIND_MIN_LEVEL = {"near": 1, "mid": 2, "far": 3}
|
||||
# Pioggia: mm/3h minimi per fascia
|
||||
LEAD_RAIN_3H = {"near": RAIN_3H_LIMIT, "mid": 40.0, "far": 55.0}
|
||||
|
||||
# ----------------- FILES -----------------
|
||||
STATE_FILE = "/home/daniely/docker/telegram-bot/weather_state.json"
|
||||
@@ -232,6 +272,18 @@ def telegram_send_html(message_html: str, chat_ids: Optional[List[str]] = None)
|
||||
message_html: Messaggio HTML da inviare
|
||||
chat_ids: Lista di chat IDs (default: TELEGRAM_CHAT_IDS)
|
||||
"""
|
||||
try:
|
||||
from telegram_gate import mirror_alert_to_web, telegram_alerts_enabled
|
||||
except ImportError:
|
||||
telegram_alerts_enabled = lambda: True # type: ignore
|
||||
mirror_alert_to_web = lambda *a, **k: False # type: ignore
|
||||
|
||||
if not telegram_alerts_enabled():
|
||||
LOGGER.info("Telegram sospeso: skip severe_weather")
|
||||
if message_html:
|
||||
mirror_alert_to_web(message_html, "severe_weather", "warning", is_html=True)
|
||||
return False
|
||||
|
||||
token = load_bot_token()
|
||||
if not token:
|
||||
LOGGER.warning("Telegram token missing: message not sent.")
|
||||
@@ -1159,9 +1211,32 @@ def format_convective_alert(storm_events: List[Dict], times: List[str], start_id
|
||||
# =============================================================================
|
||||
# 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."""
|
||||
def lead_tier(lead_hours: float) -> str:
|
||||
"""Fascia di anticipo: near (<6h), mid (6–12h), far (≥12h)."""
|
||||
if lead_hours < LEAD_NEAR_H:
|
||||
return "near"
|
||||
if lead_hours < LEAD_MID_H:
|
||||
return "mid"
|
||||
return "far"
|
||||
|
||||
|
||||
def lead_hours_from_start_str(start_str: str, now: datetime.datetime) -> float:
|
||||
"""Calcola ore di anticipo da stringa 'dd/mm HH:MM' (o vuota → 0)."""
|
||||
if not start_str:
|
||||
return 0.0
|
||||
try:
|
||||
# Formato ddmmyy_hhmm: "dd/mm HH:MM" — anno corrente / prossimo
|
||||
dt = datetime.datetime.strptime(start_str.strip(), "%d/%m %H:%M")
|
||||
dt = dt.replace(year=now.year, tzinfo=TZINFO)
|
||||
if dt < now - datetime.timedelta(days=1):
|
||||
dt = dt.replace(year=now.year + 1)
|
||||
return max(0.0, (dt - now).total_seconds() / 3600.0)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def storm_event_significance(event: Dict, lead_hours: float = 0.0) -> List[str]:
|
||||
"""Ritorna i motivi di significatività; soglie scalate per anticipo (lead_hours)."""
|
||||
reasons: List[str] = []
|
||||
threats = event.get("threats", []) or []
|
||||
cape = float(event.get("cape", 0.0) or 0.0)
|
||||
@@ -1169,39 +1244,59 @@ def storm_event_significance(event: Dict) -> List[str]:
|
||||
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)
|
||||
gusts = float(event.get("gusts", 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
|
||||
thr = LEAD_STORM[lead_tier(lead_hours)]
|
||||
|
||||
lightning_precip = (precip >= thr["lightning_precip_h"] or
|
||||
precip_3h >= thr["lightning_precip_3h"])
|
||||
if ("Fulminazioni" in threats and cape >= thr["cape"]
|
||||
and lpi > 0 and lightning_precip):
|
||||
reasons.append("Fulminazioni forti")
|
||||
if precip >= SIGNIFICANT_PRECIP_H or precip_3h >= SIGNIFICANT_PRECIP_3H:
|
||||
if precip >= thr["precip_h"] or precip_3h >= thr["precip_3h"]:
|
||||
reasons.append("Bomba d'acqua")
|
||||
if "Downburst/Temporale violento" in threats:
|
||||
if ("Downburst/Temporale violento" in threats
|
||||
and gusts >= thr["downburst_gusts"]
|
||||
and cape >= thr["cape"]):
|
||||
reasons.append("Downburst")
|
||||
if score >= SIGNIFICANT_STORM_SCORE and not reasons:
|
||||
if score >= thr["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, con cluster minimo e anti-glitch."""
|
||||
"""Mantiene solo eventi significativi per la loro fascia di anticipo + cluster."""
|
||||
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
|
||||
if lead < 0:
|
||||
lead = 0.0
|
||||
reasons = storm_event_significance(ev, lead_hours=lead)
|
||||
if not reasons:
|
||||
continue
|
||||
e2 = dict(ev)
|
||||
e2["lead_hours"] = lead
|
||||
e2["lead_tier"] = lead_tier(lead)
|
||||
e2["significance"] = reasons
|
||||
out.append(e2)
|
||||
return filter_storm_cluster(out)
|
||||
|
||||
|
||||
def wind_meets_lead_threshold(level: int, lead_hours: float) -> bool:
|
||||
"""Vento notificato solo se il livello raggiunge il minimo della fascia di anticipo."""
|
||||
if level <= 0:
|
||||
return False
|
||||
return level >= LEAD_WIND_MIN_LEVEL[lead_tier(lead_hours)]
|
||||
|
||||
|
||||
def rain_meets_lead_threshold(rain_max_3h: float, lead_hours: float) -> bool:
|
||||
"""Pioggia notificata solo se mm/3h raggiungono la soglia della fascia di anticipo."""
|
||||
return rain_max_3h >= LEAD_RAIN_3H[lead_tier(lead_hours)]
|
||||
|
||||
|
||||
def _today_key(now: datetime.datetime) -> str:
|
||||
return now.strftime("%Y-%m-%d")
|
||||
|
||||
@@ -1634,21 +1729,37 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
|
||||
state["convective_storm_active"] = False
|
||||
state["last_storm_score"] = 0.0
|
||||
|
||||
# 2) Vento persistente
|
||||
if wind_level_curr > 0:
|
||||
# 2) Vento persistente — soglia livello scalata per anticipo
|
||||
wind_lead = lead_hours_from_start_str(wind_start, now) if wind_level_curr > 0 else 0.0
|
||||
wind_notify = wind_meets_lead_threshold(wind_level_curr, wind_lead)
|
||||
if wind_notify:
|
||||
wind_msg = wind_message(wind_level_curr, wind_peak, wind_start, wind_run_len)
|
||||
tier = lead_tier(wind_lead)
|
||||
if tier != "near":
|
||||
wind_msg += f"\n🔭 <i>Anticipo ~{wind_lead:.0f}h (fascia {tier}: soglia elevata)</i>"
|
||||
if "wind" in comparisons:
|
||||
comp = comparisons["wind"]
|
||||
wind_msg += f"\n⚠️ <b>Discordanza modelli</b>: AROME {comp['arome']:.0f} km/h | ICON {comp['icon']:.0f} km/h (scostamento {comp['diff_pct']:.0f}%)"
|
||||
alerts.append(wind_msg)
|
||||
state["wind_level"] = wind_level_curr
|
||||
state["last_wind_peak"] = float(wind_peak)
|
||||
elif wind_level_curr > 0:
|
||||
LOGGER.info(
|
||||
"Vento livello %s soppresso (anticipo ~%.0fh, fascia %s richiede livello ≥%s)",
|
||||
wind_level_curr, wind_lead, lead_tier(wind_lead),
|
||||
LEAD_WIND_MIN_LEVEL[lead_tier(wind_lead)],
|
||||
)
|
||||
state["wind_level"] = 0
|
||||
state["last_wind_peak"] = 0.0
|
||||
else:
|
||||
state["wind_level"] = 0
|
||||
state["last_wind_peak"] = 0.0
|
||||
|
||||
# 3) Pioggia persistente
|
||||
if rain_persist >= PERSIST_HOURS and rain_max_3h >= RAIN_3H_LIMIT:
|
||||
# 3) Pioggia persistente — mm/3h scalati per anticipo
|
||||
rain_lead = lead_hours_from_start_str(rain_start, now) if rain_persist >= PERSIST_HOURS else 0.0
|
||||
rain_base_ok = rain_persist >= PERSIST_HOURS and rain_max_3h >= RAIN_3H_LIMIT
|
||||
rain_notify = rain_base_ok and rain_meets_lead_threshold(rain_max_3h, rain_lead)
|
||||
if rain_notify:
|
||||
rain_analysis = None
|
||||
if rain_start:
|
||||
rain_start_idx = -1
|
||||
@@ -1670,18 +1781,31 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
|
||||
threshold_mm_h=8.0,
|
||||
)
|
||||
rain_msg = rain_message(rain_max_3h, rain_start, rain_persist, rain_analysis=rain_analysis)
|
||||
tier = lead_tier(rain_lead)
|
||||
if tier != "near":
|
||||
rain_msg += (
|
||||
f"\n🔭 <i>Anticipo ~{rain_lead:.0f}h (fascia {tier}: "
|
||||
f"soglia {LEAD_RAIN_3H[tier]:.0f} mm/3h)</i>"
|
||||
)
|
||||
if "rain" in comparisons:
|
||||
comp = comparisons["rain"]
|
||||
rain_msg += f"\n⚠️ <b>Discordanza modelli</b>: AROME {comp['arome']:.1f} mm | ICON {comp['icon']:.1f} mm (scostamento {comp['diff_pct']:.0f}%)"
|
||||
alerts.append(rain_msg)
|
||||
state["last_rain_3h"] = float(rain_max_3h)
|
||||
elif rain_base_ok:
|
||||
LOGGER.info(
|
||||
"Pioggia %.1f mm/3h soppressa (anticipo ~%.0fh, fascia %s richiede ≥%.0f mm/3h)",
|
||||
rain_max_3h, rain_lead, lead_tier(rain_lead),
|
||||
LEAD_RAIN_3H[lead_tier(rain_lead)],
|
||||
)
|
||||
state["last_rain_3h"] = 0.0
|
||||
else:
|
||||
state["last_rain_3h"] = 0.0
|
||||
|
||||
is_alarm_now = (
|
||||
(len(sig_storms) > 0)
|
||||
or (wind_level_curr > 0)
|
||||
or (rain_persist >= PERSIST_HOURS and rain_max_3h >= RAIN_3H_LIMIT)
|
||||
or wind_notify
|
||||
or rain_notify
|
||||
)
|
||||
|
||||
should_notify = bool(alerts)
|
||||
@@ -1692,7 +1816,12 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
|
||||
should_notify = True
|
||||
debug_message_only = True
|
||||
|
||||
alert_signature = build_alert_signature(alerts, wind_level_curr, rain_max_3h, sig_storms)
|
||||
alert_signature = build_alert_signature(
|
||||
alerts,
|
||||
wind_level_curr if wind_notify else 0,
|
||||
rain_max_3h if rain_notify else 0.0,
|
||||
sig_storms,
|
||||
)
|
||||
|
||||
# --- Gate anti-spam globale (max 1/giorno + cooldown, escalation se peggioramento) ---
|
||||
if should_notify and alerts and not debug_message_only:
|
||||
@@ -1741,10 +1870,10 @@ def analyze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, lat:
|
||||
alert_types = []
|
||||
if sig_storms and len(sig_storms) > 0:
|
||||
alert_types.append("TEMPORALI SEVERI")
|
||||
if wind_level_curr > 0:
|
||||
if wind_notify:
|
||||
wind_labels = {3: "TEMPESTA", 2: "VENTO MOLTO FORTE", 1: "VENTO FORTE"}
|
||||
alert_types.append(wind_labels.get(wind_level_curr, "VENTO FORTE"))
|
||||
if rain_persist >= PERSIST_HOURS and rain_max_3h >= RAIN_3H_LIMIT:
|
||||
if rain_notify:
|
||||
alert_types.append("PIOGGIA INTENSA")
|
||||
|
||||
state["alert_active"] = True
|
||||
|
||||
Reference in New Issue
Block a user