Backup automatico script del 2026-01-01 14:12
This commit is contained in:
@@ -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} {'T°':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30]
|
lines = [f"{'LT':<2} {'T°':>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))
|
||||||
|
|
||||||
|
|||||||
227
services/telegram-bot/check_ghiaccio.py
Normal file
227
services/telegram-bot/check_ghiaccio.py
Normal 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()
|
||||||
Reference in New Issue
Block a user