Backup automatico script del 2026-02-08 07:00

This commit is contained in:
2026-02-08 07:00:03 +01:00
parent d79431ed28
commit 812bcd002c
8 changed files with 1911 additions and 446 deletions
+83 -40
View File
@@ -19,15 +19,14 @@ from open_meteo_client import configure_open_meteo_session
# snow_radar.py
#
# Scopo:
# Analizza la nevicata in una griglia di località in un raggio di 40km da San Marino.
# Per ciascuna località mostra:
# - Nome della località
# - Somma dello snowfall orario nelle 12 ore precedenti
# - Somma dello snowfall previsto nelle 12 ore successive
# - Somma dello snowfall previsto nelle 24 ore successive
# Analizza la neve in una griglia di località in un raggio di 40km da San Marino.
# Combina due parametri Open-Meteo:
# - snowfall: precipitazione nevosa (cm/h) - neve che cade
# - snow_depth: spessore manto al suolo (m) - neve accumulata, incluso residuo
# senza precipitazione (es. giorni successivi a nevicata)
#
# Modello meteo:
# meteofrance_seamless (AROME) per dati dettagliati
# italia_meteo_arpae_icon_2i (supporta snowfall e snow_depth)
#
# Token Telegram:
# Nessun token in chiaro. Lettura in ordine:
@@ -88,8 +87,14 @@ LOCATIONS = [
TZ = "Europe/Berlin"
TZINFO = ZoneInfo(TZ)
# Modello meteo
MODEL_AROME = "meteofrance_seamless"
# Modello meteo: italia_meteo_arpae_icon_2i supporta snowfall e snow_depth
# - snowfall: precipitazione nevosa (cm/h)
# - snow_depth: spessore manto al suolo (m), include neve residua anche senza precipitazione
MODEL_SNOW = "italia_meteo_arpae_icon_2i"
# Soglia minima (cm) per considerare "neve presente" - evita falsi positivi da rumore/modello
# Valori < 1 cm sono tracce trascurabili (dew, frost, errori numerici) - non neve reale
SNOW_THRESHOLD_CM = 1.0
# File di log
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -189,14 +194,16 @@ def calculate_distance_km(lat1: float, lon1: float, lat2: float, lon2: float) ->
def get_forecast(session: requests.Session, lat: float, lon: float) -> Optional[Dict]:
"""
Recupera previsioni meteo per una località.
Inclusi: snowfall (precipitazione nevosa cm/h), snow_depth (manto al suolo m).
"""
params = {
"latitude": lat,
"longitude": lon,
"hourly": "snowfall,weathercode",
"hourly": "snowfall,snow_depth,weathercode",
"timezone": TZ,
"forecast_days": 2,
"models": MODEL_AROME,
"past_days": 7,
"forecast_days": 7,
"models": MODEL_SNOW,
}
try:
@@ -217,22 +224,25 @@ def get_forecast(session: requests.Session, lat: float, lon: float) -> Optional[
def analyze_snowfall_for_location(data: Dict, now: datetime.datetime) -> Optional[Dict]:
"""
Analizza snowfall per una località.
Analizza snowfall e snow_depth per una località.
Nota: Open-Meteo fornisce principalmente dati futuri. Per i dati passati,
includiamo anche le ore appena passate se disponibili nei dati hourly.
Combina:
- snowfall: precipitazione nevosa (cm/h) - neve che cade
- snow_depth: spessore manto al suolo (m) - neve accumulata, incluso residuo senza precipitazione
Returns:
Dict con:
- snow_past_12h: somma snowfall ultime 12 ore (cm) - se disponibile nei dati
- snow_next_12h: somma snowfall prossime 12 ore (cm)
- snow_next_24h: somma snowfall prossime 24 ore (cm)
Dict con valori in cm:
- snow_past_12h: max(somma snowfall ultime 12h, snow_depth attuale)
- snow_next_12h: somma snowfall prossime 12h
- snow_next_24h: max(somma snowfall prossime 24h, snow_depth max previsto)
- snow_depth_now_cm: manto attuale al suolo (cm)
"""
hourly = data.get("hourly", {}) or {}
times = hourly.get("time", []) or []
snowfall = hourly.get("snowfall", []) or []
snow_depth_raw = hourly.get("snow_depth", []) or [] # in metri (m)
if not times or not snowfall:
if not times:
return None
# Converti timestamps
@@ -243,29 +253,54 @@ def analyze_snowfall_for_location(data: Dict, now: datetime.datetime) -> Optiona
next_12h_end = now + datetime.timedelta(hours=12)
next_24h_end = now + datetime.timedelta(hours=24)
snow_past_12h = 0.0
snow_next_12h = 0.0
snow_next_24h = 0.0
snowfall_past_12h = 0.0
snowfall_next_12h = 0.0
snowfall_next_24h = 0.0
snow_depth_now_m = 0.0
snow_depth_max_past_12h_m = 0.0
snow_depth_max_next_24h_m = 0.0
# Rumore numerico: valori < 0.01 cm (snowfall) o < 0.0001 m (snow_depth) → 0
NOISE_FLOOR_SNOWFALL_CM = 0.01
NOISE_FLOOR_SNOW_DEPTH_M = 0.0001
for i, dt in enumerate(dt_list):
snow_val = float(snowfall[i]) if i < len(snowfall) and snowfall[i] is not None else 0.0
depth_val = float(snow_depth_raw[i]) if i < len(snow_depth_raw) and snow_depth_raw[i] is not None else 0.0
if snow_val < NOISE_FLOOR_SNOWFALL_CM:
snow_val = 0.0
if depth_val < NOISE_FLOOR_SNOW_DEPTH_M:
depth_val = 0.0
# Ultime 12 ore (passato) - solo se i dati includono il passato
# Snowfall nelle finestre temporali
if dt < now and dt >= past_12h_start:
snow_past_12h += snow_val
# Prossime 12 ore
snowfall_past_12h += snow_val
snow_depth_max_past_12h_m = max(snow_depth_max_past_12h_m, depth_val)
if now <= dt < next_12h_end:
snow_next_12h += snow_val
# Prossime 24 ore
snowfall_next_12h += snow_val
if now <= dt < next_24h_end:
snow_next_24h += snow_val
snowfall_next_24h += snow_val
snow_depth_max_next_24h_m = max(snow_depth_max_next_24h_m, depth_val)
# snow_depth attuale: usa valore più vicino a "now" (ultima ora passata o prima futura)
if dt <= now:
snow_depth_now_m = depth_val
# snow_depth da m a cm
snow_depth_now_cm = snow_depth_now_m * 100.0
snow_depth_max_past_12h_cm = snow_depth_max_past_12h_m * 100.0
snow_depth_max_next_24h_cm = snow_depth_max_next_24h_m * 100.0
# Combina precipitazione + manto: per passato usa max(somma precipitazione, manto attuale)
# per futuro usa max(somma precipitazione, manto max previsto)
snow_past_12h = max(snowfall_past_12h, snow_depth_now_cm)
snow_next_24h = max(snowfall_next_24h, snow_depth_max_next_24h_cm)
return {
"snow_past_12h": snow_past_12h,
"snow_next_12h": snow_next_12h,
"snow_next_12h": snowfall_next_12h,
"snow_next_24h": snow_next_24h,
"snow_depth_now_cm": snow_depth_now_cm,
}
@@ -314,6 +349,9 @@ def generate_snow_map(results: List[Dict], center_lat: float, center_lon: float,
totals = [r.get(data_field, 0.0) for r in results]
max_total = max(totals) if totals else 1.0
min_total = min(totals) if totals else 0.0
# Evita vmin==vmax (divisione per zero nel colormap) - tutti 0 → scala 0..1
if max_total <= min_total:
max_total = max(min_total + 0.1, 1.0)
# Estrai coordinate
lats = [r["lat"] for r in results]
@@ -457,9 +495,10 @@ def generate_snow_map(results: List[Dict], center_lat: float, center_lon: float,
ax.legend(handles=legend_elements, loc='lower left', fontsize=10,
framealpha=0.95, edgecolor='black', fancybox=True, shadow=True)
# Info timestamp spostata in alto a destra
# Info timestamp spostata in alto a destra (Località con neve = solo quelle con neve sopra soglia)
now = now_local()
info_text = f"Aggiornamento: {now.strftime('%d/%m/%Y %H:%M')}\nLocalità con neve: {len(results)}"
num_with_snow = sum(1 for r in results if r.get("has_snow", False))
info_text = f"Aggiornamento: {now.strftime('%d/%m/%Y %H:%M')}\nLocalità con neve: {num_with_snow}"
ax.text(0.98, 0.98, info_text, transform=ax.transAxes,
fontsize=9, verticalalignment='top', horizontalalignment='right',
bbox=dict(boxstyle='round,pad=0.5', facecolor='white', alpha=0.9,
@@ -660,11 +699,13 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id
continue
# Aggiungi sempre Casa, anche se non c'è neve
# Per le altre località, aggiungi solo se c'è neve (passata o prevista)
# Per le altre località, aggiungi solo se c'è neve sopra soglia (precipitazione o manto residuo)
is_casa = loc["name"] == "Casa (Strada Cà Toro)"
has_snow = (snow_analysis["snow_past_12h"] > 0.0 or
snow_analysis["snow_next_12h"] > 0.0 or
snow_analysis["snow_next_24h"] > 0.0)
has_snow = (
snow_analysis["snow_past_12h"] >= SNOW_THRESHOLD_CM or
snow_analysis["snow_next_12h"] >= SNOW_THRESHOLD_CM or
snow_analysis["snow_next_24h"] >= SNOW_THRESHOLD_CM
)
if is_casa or has_snow:
results.append({
@@ -672,6 +713,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id
"lat": loc["lat"],
"lon": loc["lon"],
"distance_km": distance_km,
"has_snow": has_snow,
**snow_analysis
})
@@ -687,6 +729,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id
# Genera e invia DUE mappe separate
now_str = now.strftime('%d/%m/%Y %H:%M')
num_with_snow = sum(1 for r in results if r.get("has_snow", False))
# 1. Mappa snowfall passato (12h precedenti)
map_path_past = os.path.join(BASE_DIR, "snow_radar_past.png")
@@ -698,7 +741,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id
f"❄️ *SNOW RADAR - Ultime 12h*\n"
f"📍 Centro: San Marino\n"
f"🕒 {now_str}\n"
f"📊 Località con neve: {len(results)}/{len(LOCATIONS)}"
f"📊 Località con neve: {num_with_snow}/{len(LOCATIONS)}"
)
telegram_send_photo(map_path_past, caption_past, chat_ids=chat_ids)
# Pulisci file temporaneo
@@ -718,7 +761,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id
f"❄️ *SNOW RADAR - Prossime 24h*\n"
f"📍 Centro: San Marino\n"
f"🕒 {now_str}\n"
f"📊 Località con neve: {len(results)}/{len(LOCATIONS)}"
f"📊 Località con neve: {num_with_snow}/{len(LOCATIONS)}"
)
telegram_send_photo(map_path_future, caption_future, chat_ids=chat_ids)
# Pulisci file temporaneo