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)
# - Dashboard Sistema (SSH/Ping)
# - Meteo Arome ASCII (On-Demand + Schedulato)
# LOOGLE BOT V7.9 (ULTIMATE + GLOBAL GFS FIX)
# - Dashboard Sistema
# - Meteo Smart: Arome (EU), Icon (EU-Est), JMA (JP), GFS (Mondo)
# - Multi-User Security
# =============================================================================
@@ -45,8 +45,7 @@ TZINFO = ZoneInfo(TZ)
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
MODEL = "meteofrance_arome_france_hd"
HTTP_HEADERS = {"User-Agent": "loogle-bot-v7"}
HTTP_HEADERS = {"User-Agent": "loogle-bot-v7.9"}
# --- LISTE DISPOSITIVI ---
CORE_DEVICES = [
@@ -123,7 +122,7 @@ def run_speedtest():
except: return "Errore Speedtest"
# =============================================================================
# SEZIONE 2: FUNZIONI METEO (AROME ASCII)
# SEZIONE 2: METEO INTELLIGENTE (MULTI-MODELLO MOSAICO)
# =============================================================================
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 = "💨"
return sky, sgx
def get_coordinates(city_name: str) -> Optional[Tuple[float, float, str]]:
params = {"name": city_name, "count": 1, "language": "it", "format": "json"}
def get_coordinates(city_name: str):
"""
Cerca le coordinate della città con fallback EN.
"""
# 1. Tentativo ITALIANO
params = {"name": city_name, "count": 10, "language": "it", "format": "json"}
try:
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]
name = f"{res.get('name')} ({res.get('country_code','')})"
return res["latitude"], res["longitude"], name
except Exception as e: logger.error(f"Geocoding error: {e}")
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 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
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 = {
"latitude": lat, "longitude": lon, "timezone": TZ,
"forecast_days": 3, "models": MODEL,
"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",
"latitude": lat, "longitude": lon, "timezone": TZ,
"forecast_days": 3,
"models": model,
"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:
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()
except Exception as e: logger.error(f"Meteo API error: {e}"); return None
def generate_weather_report(lat, lon, location_name) -> str:
data = get_forecast(lat, lon)
if not data: return "❌ Errore API Meteo."
def safe_get_list(hourly_data, key, length, default=None):
"""Estrae una lista sicura, gestendo chiavi mancanti"""
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", {})
times = hourly.get("time", [])
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)
blocks = []
@@ -198,7 +282,6 @@ def generate_weather_report(lat, lon, location_name) -> str:
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]
count = 0
for i, t_str in enumerate(times):
try: dt = parse_time(t_str)
except: continue
@@ -206,48 +289,41 @@ def generate_weather_report(lat, lon, location_name) -> str:
if dt.hour % step != 0: continue
try:
T = float(hourly["temperature_2m"][i])
Rh = int(hourly["relative_humidity_2m"][i] or 0)
Cl = int(hourly["cloudcover"][i] or 0)
Pr = float(hourly["precipitation"][i] or 0)
Rn = float(hourly["rain"][i] or 0)
Sn = float(hourly["snowfall"][i] or 0)
Wspd = float(hourly["windspeed_10m"][i] or 0)
Gust = float(hourly["windgusts_10m"][i] or 0)
Wdir = int(hourly["winddirection_10m"][i] or 0)
Cape = float(hourly["cape"][i] or 0)
Vis = float(hourly["visibility"][i] or 10000)
Code = int(hourly["weathercode"][i]) if hourly["weathercode"][i] is not None else None
IsDay = int(hourly["is_day"][i] if hourly["is_day"][i] is not None else 1)
T = float(l_temp[i])
Rh = int(l_rh[i] or 0)
Cl = int(l_cl[i] or 0)
Pr = float(l_prec[i] or 0)
Rn = float(l_rain[i] or 0)
Sn = float(l_snow[i] or 0)
Wspd = float(l_wspd[i] or 0)
Gust = float(l_gust[i] or 0)
Wdir = int(l_wdir[i] or 0)
Cape = float(l_cape[i] or 0)
Vis = float(l_vis[i] or 10000)
Code = int(l_code[i]) if l_code[i] is not None else None
IsDay = int(l_day[i] if l_day[i] is not None else 1)
t_s = f"{int(round(T))}"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}"
card = degrees_to_cardinal(Wdir)
w_val = Wspd
is_g = (Gust - Wspd) > 15
if is_g: w_val = Gust
w_val = Gust if (Gust - Wspd) > 15 else Wspd
w_txt = f"{card} {int(round(w_val))}"
if is_g:
if (Gust - Wspd) > 15:
g_txt = f"G{int(round(w_val))}"
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}"
else: w_txt = g_txt
w_fmt = f"{w_txt:<5}"
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}")
count += 1
except: continue
if count > 0:
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```")
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
@@ -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("🌤️ 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")
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)
if coords:
lat, lon, name = coords
report = generate_weather_report(lat, lon, name)
lat, lon, name, cc = coords
report = generate_weather_report(lat, lon, name, cc)
await update.message.reply_text(report, parse_mode="Markdown")
else:
await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown")
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
"""Funzione lanciata dallo scheduler alle 08:00"""
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:
try:
await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
except Exception as e:
logger.error(f"Errore invio report a {uid}: {e}")
try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
except: pass
@restricted
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":
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")]]
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")
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()
# Handlers
@@ -400,8 +474,7 @@ def main():
application.add_handler(CommandHandler("meteo", meteo_command))
application.add_handler(CallbackQueryHandler(button_handler))
# SCHEDULER (Sostituisce CRON)
# Esegue il meteo tutti i giorni alle 08:00 Europe/Rome
# SCHEDULER
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))