Backup automatico script del 2026-01-01 14:12

This commit is contained in:
2026-01-01 14:12:36 +01:00
parent 6223b2cd4b
commit 272e3cf0a5
2 changed files with 358 additions and 58 deletions

View File

@@ -18,9 +18,9 @@ from telegram.ext import (
) )
# ============================================================================= # =============================================================================
# LOOGLE BOT V7.0 (ULTIMATE) # LOOGLE BOT V7.9 (ULTIMATE + GLOBAL GFS FIX)
# - Dashboard Sistema (SSH/Ping) # - Dashboard Sistema
# - Meteo Arome ASCII (On-Demand + Schedulato) # - Meteo Smart: Arome (EU), Icon (EU-Est), JMA (JP), GFS (Mondo)
# - Multi-User Security # - Multi-User Security
# ============================================================================= # =============================================================================
@@ -45,8 +45,7 @@ TZINFO = ZoneInfo(TZ)
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search" GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
MODEL = "meteofrance_arome_france_hd" HTTP_HEADERS = {"User-Agent": "loogle-bot-v7.9"}
HTTP_HEADERS = {"User-Agent": "loogle-bot-v7"}
# --- LISTE DISPOSITIVI --- # --- LISTE DISPOSITIVI ---
CORE_DEVICES = [ CORE_DEVICES = [
@@ -123,7 +122,7 @@ def run_speedtest():
except: return "Errore Speedtest" except: return "Errore Speedtest"
# ============================================================================= # =============================================================================
# SEZIONE 2: FUNZIONI METEO (AROME ASCII) # SEZIONE 2: METEO INTELLIGENTE (MULTI-MODELLO MOSAICO)
# ============================================================================= # =============================================================================
def now_local() -> datetime.datetime: def now_local() -> datetime.datetime:
@@ -158,24 +157,81 @@ def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape):
elif gust > 50: sgx = "💨" elif gust > 50: sgx = "💨"
return sky, sgx return sky, sgx
def get_coordinates(city_name: str) -> Optional[Tuple[float, float, str]]: def get_coordinates(city_name: str):
params = {"name": city_name, "count": 1, "language": "it", "format": "json"} """
Cerca le coordinate della città con fallback EN.
"""
# 1. Tentativo ITALIANO
params = {"name": city_name, "count": 10, "language": "it", "format": "json"}
try: try:
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10) r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
data = r.json() data = r.json()
if "results" in data and len(data["results"]) > 0: if "results" in data and len(data["results"]) > 0:
res = data["results"][0] res = data["results"][0]
name = f"{res.get('name')} ({res.get('country_code','')})" cc = res.get("country_code", "IT").upper()
return res["latitude"], res["longitude"], name name = f"{res.get('name')} ({cc})"
except Exception as e: logger.error(f"Geocoding error: {e}") return res["latitude"], res["longitude"], name, cc
except Exception as e: logger.error(f"Geocoding IT error: {e}")
# 2. Tentativo FALLBACK INGLESE
try:
params["language"] = "en"
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
data = r.json()
if "results" in data and len(data["results"]) > 0:
res = data["results"][0]
cc = res.get("country_code", "IT").upper()
name = f"{res.get('name')} ({cc})"
return res["latitude"], res["longitude"], name, cc
except Exception as e: logger.error(f"Geocoding EN error: {e}")
return None return None
def get_forecast(lat, lon) -> Optional[Dict]: def choose_best_model(lat, lon, cc):
"""
Seleziona il modello migliore.
Fallback Generale: NOAA GFS (perché ha sempre Visibilità/Cape).
"""
# 1. GIAPPONE -> JMA MSM
if cc == 'JP':
return "jma_msm", "JMA MSM (5km)"
# 2. SCANDINAVIA -> Yr.no
if cc in ['NO', 'SE', 'FI', 'DK', 'IS']:
return "metno_nordic", "Yr.no (Nordic)"
# 3. UK & IRLANDA -> UK Met Office
if cc in ['GB', 'IE']:
return "ukmo_global", "UK MetOffice"
# 4. TUNING ITALIA (Mosaico)
if cc == 'IT' or cc == 'SM':
# ZONA 1: Nord-Ovest, Tirreno, Sardegna (Lon <= 13.0, Lat > 40.5)
if lon <= 13.0 and lat > 40.5:
return "meteofrance_arome_france_hd", "Arome HD"
# ZONA 2: Nord-Est, Adriatico Nord/Centro
if lat >= 43.0:
return "icon_d2", "ICON-D2 (2km)"
# ZONA 3: Sud Italia -> ICON-EU (Meglio di GFS per locale)
return "icon_eu", "ICON-EU (7km)"
# 5. RESTO DEL MONDO (Europa Centrale ICON-D2)
if cc in ['DE', 'AT', 'CH', 'LI']:
return "icon_d2", "ICON-D2"
# 6. RESTO DEL MONDO -> NOAA GFS
# Usiamo GFS invece di ECMWF perché ECMWF spesso manca di 'visibility'/'cape'
# facendo crashare o svuotare il report. GFS è completo.
return "gfs_global", "NOAA GFS Global"
def get_forecast(lat, lon, model):
params = { params = {
"latitude": lat, "longitude": lon, "timezone": TZ, "latitude": lat, "longitude": lon, "timezone": TZ,
"forecast_days": 3, "models": MODEL, "forecast_days": 3,
"wind_speed_unit": "kmh", "precipitation_unit": "mm", "models": model,
"hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility", "wind_speed_unit": "kmh", "precipitation_unit": "mm",
"hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility"
} }
try: try:
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
@@ -183,13 +239,41 @@ def get_forecast(lat, lon) -> Optional[Dict]:
return r.json() return r.json()
except Exception as e: logger.error(f"Meteo API error: {e}"); return None except Exception as e: logger.error(f"Meteo API error: {e}"); return None
def generate_weather_report(lat, lon, location_name) -> str: def safe_get_list(hourly_data, key, length, default=None):
data = get_forecast(lat, lon) """Estrae una lista sicura, gestendo chiavi mancanti"""
if not data: return "❌ Errore API Meteo." if key in hourly_data and hourly_data[key] is not None:
return hourly_data[key]
return [default] * length
def generate_weather_report(lat, lon, location_name, cc="IT") -> str:
model_id, model_name = choose_best_model(lat, lon, cc)
data = get_forecast(lat, lon, model_id)
if not data: return f"❌ Errore API Meteo ({model_name})."
hourly = data.get("hourly", {}) hourly = data.get("hourly", {})
times = hourly.get("time", []) times = hourly.get("time", [])
if not times: return "❌ Dati orari mancanti." if not times: return "❌ Dati orari mancanti."
L = len(times)
try:
l_temp = safe_get_list(hourly, "temperature_2m", L, 0)
l_rh = safe_get_list(hourly, "relative_humidity_2m", L, 0)
l_cl = safe_get_list(hourly, "cloudcover", L, 0)
l_prec = safe_get_list(hourly, "precipitation", L, 0)
l_rain = safe_get_list(hourly, "rain", L, 0)
l_snow = safe_get_list(hourly, "snowfall", L, 0)
l_wspd = safe_get_list(hourly, "windspeed_10m", L, 0)
l_gust = safe_get_list(hourly, "windgusts_10m", L, 0)
l_wdir = safe_get_list(hourly, "winddirection_10m", L, 0)
l_code = safe_get_list(hourly, "weathercode", L, 0)
l_day = safe_get_list(hourly, "is_day", L, 1)
l_cape = safe_get_list(hourly, "cape", L, 0)
l_vis = safe_get_list(hourly, "visibility", L, 10000)
except Exception as e:
return f"❌ Errore elaborazione dati meteo: {e}"
now = now_local().replace(minute=0, second=0, microsecond=0) now = now_local().replace(minute=0, second=0, microsecond=0)
blocks = [] blocks = []
@@ -198,7 +282,6 @@ def generate_weather_report(lat, lon, location_name) -> str:
end_time = now + datetime.timedelta(hours=hours_duration) end_time = now + datetime.timedelta(hours=hours_duration)
lines = [f"{'LT':<2} {'':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30] lines = [f"{'LT':<2} {'':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30]
count = 0 count = 0
for i, t_str in enumerate(times): for i, t_str in enumerate(times):
try: dt = parse_time(t_str) try: dt = parse_time(t_str)
except: continue except: continue
@@ -206,48 +289,41 @@ def generate_weather_report(lat, lon, location_name) -> str:
if dt.hour % step != 0: continue if dt.hour % step != 0: continue
try: try:
T = float(hourly["temperature_2m"][i]) T = float(l_temp[i])
Rh = int(hourly["relative_humidity_2m"][i] or 0) Rh = int(l_rh[i] or 0)
Cl = int(hourly["cloudcover"][i] or 0) Cl = int(l_cl[i] or 0)
Pr = float(hourly["precipitation"][i] or 0) Pr = float(l_prec[i] or 0)
Rn = float(hourly["rain"][i] or 0) Rn = float(l_rain[i] or 0)
Sn = float(hourly["snowfall"][i] or 0) Sn = float(l_snow[i] or 0)
Wspd = float(hourly["windspeed_10m"][i] or 0) Wspd = float(l_wspd[i] or 0)
Gust = float(hourly["windgusts_10m"][i] or 0) Gust = float(l_gust[i] or 0)
Wdir = int(hourly["winddirection_10m"][i] or 0) Wdir = int(l_wdir[i] or 0)
Cape = float(hourly["cape"][i] or 0) Cape = float(l_cape[i] or 0)
Vis = float(hourly["visibility"][i] or 10000) Vis = float(l_vis[i] or 10000)
Code = int(hourly["weathercode"][i]) if hourly["weathercode"][i] is not None else None Code = int(l_code[i]) if l_code[i] is not None else None
IsDay = int(hourly["is_day"][i] if hourly["is_day"][i] is not None else 1) IsDay = int(l_day[i] if l_day[i] is not None else 1)
t_s = f"{int(round(T))}" t_s = f"{int(round(T))}"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}" p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}"
card = degrees_to_cardinal(Wdir) card = degrees_to_cardinal(Wdir)
w_val = Wspd w_val = Gust if (Gust - Wspd) > 15 else Wspd
is_g = (Gust - Wspd) > 15
if is_g: w_val = Gust
w_txt = f"{card} {int(round(w_val))}" w_txt = f"{card} {int(round(w_val))}"
if is_g: if (Gust - Wspd) > 15:
g_txt = f"G{int(round(w_val))}" g_txt = f"G{int(round(w_val))}"
if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}" if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}"
elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}" elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}"
else: w_txt = g_txt else: w_txt = g_txt
w_fmt = f"{w_txt:<5}" w_fmt = f"{w_txt:<5}"
sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape) sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape)
lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}") lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}")
count += 1 count += 1
except: continue except: continue
if count > 0: if count > 0:
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}" day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}"
blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```") blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```")
now = end_time now = end_time
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n\n" + "\n\n".join(blocks) return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks)
# ============================================================================= # =============================================================================
# SEZIONE 3: BOT HANDLERS & SCHEDULER # SEZIONE 3: BOT HANDLERS & SCHEDULER
@@ -271,7 +347,7 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
[InlineKeyboardButton("🛡️ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")], [InlineKeyboardButton("🛡️ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")],
[InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")] [InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
] ]
text = "🎛 **Loogle Control Center v7.0**\nComandi disponibili:\n🔹 `/meteo <città>`\n🔹 Pulsanti sotto" text = "🎛 **Loogle Control Center v7.9**\nComandi disponibili:\n🔹 `/meteo <città>`\n🔹 Pulsanti sotto"
if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
else: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") else: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
@@ -287,22 +363,19 @@ async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
coords = get_coordinates(city) coords = get_coordinates(city)
if coords: if coords:
lat, lon, name = coords lat, lon, name, cc = coords
report = generate_weather_report(lat, lon, name) report = generate_weather_report(lat, lon, name, cc)
await update.message.reply_text(report, parse_mode="Markdown") await update.message.reply_text(report, parse_mode="Markdown")
else: else:
await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown") await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown")
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None: async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
"""Funzione lanciata dallo scheduler alle 08:00"""
logger.info("⏰ Invio report automatico meteo...") logger.info("⏰ Invio report automatico meteo...")
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME) # Forza "SM" per casa -> Arome/IconD2 in base alla posizione
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, "SM")
for uid in ALLOWED_IDS: for uid in ALLOWED_IDS:
try: try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown") except: pass
except Exception as e:
logger.error(f"Errore invio report a {uid}: {e}")
@restricted @restricted
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -314,7 +387,8 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
elif data == "req_meteo_home": elif data == "req_meteo_home":
await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown") await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown")
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME) # Forza "SM" per casa
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, "SM")
keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
@@ -392,7 +466,7 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown") await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown")
def main(): def main():
logger.info("Avvio Loogle Bot v7.0 (Ultimate)...") logger.info("Avvio Loogle Bot v7.9 (Global GFS Fix)...")
application = Application.builder().token(BOT_TOKEN).build() application = Application.builder().token(BOT_TOKEN).build()
# Handlers # Handlers
@@ -400,8 +474,7 @@ def main():
application.add_handler(CommandHandler("meteo", meteo_command)) application.add_handler(CommandHandler("meteo", meteo_command))
application.add_handler(CallbackQueryHandler(button_handler)) application.add_handler(CallbackQueryHandler(button_handler))
# SCHEDULER (Sostituisce CRON) # SCHEDULER
# Esegue il meteo tutti i giorni alle 08:00 Europe/Rome
job_queue = application.job_queue job_queue = application.job_queue
job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6)) job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6))

View File

@@ -0,0 +1,227 @@
import requests
import datetime
import os
import sys
import json
# --- TELEGRAM CONFIG ---
ADMIN_CHAT_ID = "64463169"
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
# FILES
TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token")
TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token"
STATE_FILE = os.path.expanduser("~/.ghiaccio_multimodel_state.json")
# --- CONFIGURAZIONE GRIGLIA ---
GRID_POINTS = [
{"id": "G01", "name": "Nord-Est (Dogana/Falciano)", "lat": 43.9850, "lon": 12.4950},
{"id": "G02", "name": "Nord (Serravalle/Galazzano)", "lat": 43.9680, "lon": 12.4780},
{"id": "G03", "name": "Zona Ind. Ovest (Gualdicciolo)", "lat": 43.9480, "lon": 12.4180},
{"id": "G04", "name": "Ovest (Chiesanuova/Confine)", "lat": 43.9150, "lon": 12.4220},
{"id": "G05", "name": "Centro-Est (Domagnano/Valdragone)","lat": 43.9480, "lon": 12.4650},
{"id": "G06", "name": "Centro-Ovest (Acquaviva/Ventoso)", "lat": 43.9420, "lon": 12.4350},
{"id": "G07", "name": "Monte Titano (Città/Murata)", "lat": 43.9300, "lon": 12.4480},
{"id": "G08", "name": "Sotto-Monte (Borgo/Cailungo)", "lat": 43.9550, "lon": 12.4500},
{"id": "G09", "name": "Valle Est (Faetano/Corianino)", "lat": 43.9280, "lon": 12.4980},
{"id": "G10", "name": "Sud-Ovest (Fiorentino)", "lat": 43.9080, "lon": 12.4580},
{"id": "G11", "name": "Sud-Est (Montegiardino)", "lat": 43.9020, "lon": 12.4820},
{"id": "G12", "name": "Estremo Sud (Cerbaiola)", "lat": 43.8880, "lon": 12.4650}
]
# Modelli da consultare (Nome Visualizzato : Slug API)
# 'icon_eu': Ottimo generale | 'arome_medium': Alta risoluzione orografica
MODELS_TO_CHECK = {
"ICON": "icon_eu",
"AROME": "arome_medium"
}
def get_bot_token():
paths = [TOKEN_FILE_HOME, TOKEN_FILE_ETC]
for path in paths:
if os.path.exists(path):
try:
with open(path, 'r') as f:
return f.read().strip()
except IOError:
pass
print("ERRORE: Token non trovato.")
sys.exit(1)
def load_previous_state():
if not os.path.exists(STATE_FILE):
return {}
try:
with open(STATE_FILE, 'r') as f:
return json.load(f)
except Exception:
return {}
def save_current_state(state):
try:
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
except Exception as e:
print(f"Errore salvataggio stato: {e}")
def get_weather_data(lat, lon, model_slug):
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": lat,
"longitude": lon,
"hourly": "temperature_2m,dew_point_2m,precipitation,soil_temperature_0cm,relative_humidity_2m",
"models": model_slug,
"timezone": "Europe/San_Marino",
"past_days": 0,
"forecast_days": 1
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()
except Exception:
return None
def analyze_risk(weather_data):
"""Analizza i dati di un singolo modello e ritorna rischio e dettagli."""
if not weather_data:
return 0, ""
hourly = weather_data.get("hourly", {})
times = hourly.get("time", [])
now = datetime.datetime.now()
current_hour_str = now.strftime("%Y-%m-%dT%H:00")
try:
idx = times.index(current_hour_str)
except ValueError:
return 0, ""
# Estrazione dati (gestione sicura se mancano chiavi)
try:
t_soil = hourly["soil_temperature_0cm"][idx]
t_dew = hourly["dew_point_2m"][idx]
hum = hourly["relative_humidity_2m"][idx]
start_idx = max(0, idx - 6)
precip_history = hourly["precipitation"][start_idx : idx+1]
precip_sum = sum(p for p in precip_history if p is not None)
except (KeyError, TypeError):
return 0, ""
if t_soil is None or t_dew is None:
return 0, ""
details = f"Suolo {t_soil}°C, Umid {hum}%"
if precip_sum > 0.2 and t_soil <= 0:
return 2, f"🔴 <b>GHIACCIO VIVO</b> ({details})"
elif t_soil <= 0 and t_soil <= t_dew:
return 1, f"🟡 <b>Rischio BRINA</b> ({details})"
return 0, details
def generate_maps_link(lat, lon):
return f"<a href='https://www.google.com/maps/search/?api=1&query={lat},{lon}'>[Mappa]</a>"
def send_telegram_broadcast(token, message, debug_mode=False):
base_url = f"https://api.telegram.org/bot{token}/sendMessage"
recipients = [ADMIN_CHAT_ID] if debug_mode else TELEGRAM_CHAT_IDS
if debug_mode:
message = f"🛠 <b>[DEBUG - MULTI MODEL]</b> 🛠\n{message}"
for chat_id in recipients:
try:
requests.post(base_url, data={"chat_id": chat_id, "text": message, "parse_mode": "HTML", "disable_web_page_preview": True}, timeout=5)
except Exception:
pass
def main():
DEBUG_MODE = "--debug" in sys.argv
token = get_bot_token()
previous_state = load_previous_state()
current_state = {}
new_alerts = []
solved_alerts = []
print(f"--- Check Multi-Modello {datetime.datetime.now()} ---")
for point in GRID_POINTS:
pid = point["id"]
# Variabili per aggregare i risultati dei modelli
max_risk_level = 0
triggered_models = []
alert_messages = []
# CICLO SUI MODELLI (ICON, AROME)
for model_name, model_slug in MODELS_TO_CHECK.items():
data = get_weather_data(point["lat"], point["lon"], model_slug)
risk, msg = analyze_risk(data)
if risk > 0:
triggered_models.append(model_name)
alert_messages.append(msg)
if risk > max_risk_level:
max_risk_level = risk
# Salvataggio stato (prendiamo il rischio massimo rilevato tra i modelli)
current_state[pid] = max_risk_level
old_level = previous_state.get(pid, 0)
maps_link = generate_maps_link(point["lat"], point["lon"])
# --- LOGICA NOTIFICHE ---
# 1. Nessun cambiamento di LIVELLO
if max_risk_level == old_level:
continue
# 2. Nuovo Rischio o Aggravamento
if max_risk_level > old_level:
# Creiamo una stringa che dice chi ha rilevato cosa
sources = " + ".join(triggered_models)
# Prendiamo il messaggio del rischio più alto (o il primo)
main_msg = alert_messages[0] if alert_messages else "Dati incerti"
final_msg = (f"📍 <b>{point['name']}</b> {maps_link}\n"
f"{main_msg}\n"
f"📡 <i>Rilevato da: {sources}</i>")
new_alerts.append(final_msg)
# 3. Rischio Cessato (Tutti i modelli danno verde)
elif max_risk_level == 0 and old_level > 0:
solved_alerts.append(f"✅ <b>{point['name']}</b> {maps_link}: Rischio rientrato (Tutti i modelli).")
# 4. Aggiornamento (es. Da Ghiaccio a Brina)
elif max_risk_level > 0:
sources = " + ".join(triggered_models)
main_msg = alert_messages[0]
new_alerts.append(f"📍 <b>{point['name']}</b> {maps_link} [AGGIORNAMENTO]\n{main_msg}\n📡 <i>Fonte: {sources}</i>")
# Invio
messages_to_send = []
if new_alerts:
messages_to_send.append("❄️ <b>ALLERTA GHIACCIO STRADALE</b> ❄️\n" + "\n\n".join(new_alerts))
if solved_alerts:
messages_to_send.append(" <b>ALLARMI CESSATI</b>\n" + "\n".join(solved_alerts))
if messages_to_send:
full_message = "\n\n".join(messages_to_send)
send_telegram_broadcast(token, full_message, debug_mode=DEBUG_MODE)
print("Notifiche inviate.")
else:
print("Nessuna variazione.")
if DEBUG_MODE:
send_telegram_broadcast(token, "Nessuna variazione (Check Debug OK).", debug_mode=True)
if not DEBUG_MODE:
save_current_state(current_state)
if __name__ == "__main__":
main()