import logging import subprocess import os import datetime import requests import shlex import json import time from functools import wraps from typing import Optional from zoneinfo import ZoneInfo from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CommandHandler, CallbackQueryHandler, ContextTypes, JobQueue ) # ============================================================================= # LOOGLE BOT V8.1 (MODULARE + ON-DEMAND METEO) # ============================================================================= # --- CONFIGURAZIONE --- BOT_TOKEN = os.environ.get('BOT_TOKEN') allowed_users_raw = os.environ.get('ALLOWED_USER_ID', '') ALLOWED_IDS = [int(x.strip()) for x in allowed_users_raw.split(',') if x.strip().isdigit()] SSH_USER = "daniely" NAS_USER = "daniely" MASTER_IP = "192.168.128.80" TZ = "Europe/Rome" TZINFO = ZoneInfo(TZ) # PERCORSI SCRIPT SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) METEO_SCRIPT = os.path.join(SCRIPT_DIR, "meteo.py") # SCRIPT METEO SEPARATO METEO7_SCRIPT = os.path.join(SCRIPT_DIR, "previsione7.py") SEVERE_SCRIPT = os.path.join(SCRIPT_DIR, "severe_weather.py") ICE_CHECK_SCRIPT = os.path.join(SCRIPT_DIR, "check_ghiaccio.py") IRRIGATION_SCRIPT = os.path.join(SCRIPT_DIR, "smart_irrigation_advisor.py") SNOW_RADAR_SCRIPT = os.path.join(SCRIPT_DIR, "snow_radar.py") # FILE STATO VIAGGI VIAGGI_STATE_FILE = os.path.join(SCRIPT_DIR, "viaggi_attivi.json") # --- LISTE DISPOSITIVI (CORE/INFRA) --- CORE_DEVICES = [ {"name": "🍓 Pi-1 (Master)", "ip": MASTER_IP, "type": "pi", "user": SSH_USER}, {"name": "🍓 Pi-2 (Backup)", "ip": "127.0.0.1", "type": "local", "user": ""}, {"name": "🗄️ NAS 920+", "ip": "192.168.128.100", "type": "nas", "user": NAS_USER}, {"name": "🗄️ NAS 214", "ip": "192.168.128.90", "type": "nas", "user": NAS_USER} ] INFRA_DEVICES = [ {"name": "📡 Router", "ip": "192.168.128.1"}, {"name": "📶 WiFi Sala", "ip": "192.168.128.101"}, {"name": "📶 WiFi Luca", "ip": "192.168.128.102"}, {"name": "📶 WiFi Taverna", "ip": "192.168.128.103"}, {"name": "📶 WiFi Dado", "ip": "192.168.128.104"}, {"name": "🔌 Sw Sala", "ip": "192.168.128.105"}, {"name": "🔌 Sw Tav", "ip": "192.168.128.106"}, {"name": "🔌 Sw Lav", "ip": "192.168.128.107"} ] logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) logger = logging.getLogger(__name__) # --- FUNZIONI SISTEMA (SSH/PING) --- def run_cmd(command, ip=None, user=None): try: if ip == "127.0.0.1" or ip is None: return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=5).decode('utf-8').strip() else: safe_cmd = command.replace("'", "'\\''") full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} '{safe_cmd}'" return subprocess.check_output(full_cmd, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip() except Exception: return "Err" def get_ping_icon(ip): try: subprocess.run(["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=0.8, check=True) return "✅" except Exception: return "🔴" def get_device_stats(device): ip, user, dtype = device['ip'], device['user'], device['type'] uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) if not uptime_raw or "Err" in uptime_raw: # Retry once to reduce transient SSH hiccups. time.sleep(0.5) uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) if not uptime_raw or "Err" in uptime_raw: # If ping is OK but SSH failed, mark as online with warning. if get_ping_icon(ip) == "✅": return "🟡 **ONLINE (SSH non raggiungibile)**" return "🔴 **OFFLINE**" uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0] temp = "N/A" if dtype in ["pi", "local"]: t = run_cmd("cat /sys/class/thermal/thermal_zone0/temp", ip, user) if t.isdigit(): temp = f"{int(t)/1000:.1f}°C" elif dtype == "nas": t = run_cmd("cat /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null || cat /sys/class/thermal/thermal_zone0/temp", ip, user) if t.isdigit(): v = int(t); temp = f"{v/1000:.1f}°C" if v > 1000 else f"{v}°C" if dtype == "nas": ram_cmd = "free | grep Mem | awk '{printf \"%.0f%%\", $3*100/$2}'" else: ram_cmd = "free -m | awk 'NR==2{if ($2>0) printf \"%.0f%%\", $3*100/$2; else print \"0%\"}'" disk_path = "/" if dtype != "nas" else "/volume1" disk_cmd = f"df -h {disk_path} | awk 'NR==2{{print $5}}'" return f"✅ **ONLINE**\n⏱️ Up: {uptime}\n🌡️ Temp: {temp} | 🧠 RAM: {run_cmd(ram_cmd, ip, user)} | 💾 Disk: {run_cmd(disk_cmd, ip, user)}" def read_log_file(filepath, lines=15): if not os.path.exists(filepath): return "⚠️ File non trovato." try: return subprocess.check_output(['tail', '-n', str(lines), filepath], stderr=subprocess.STDOUT).decode('utf-8') except Exception as e: return f"Errore: {str(e)}" def run_speedtest(): try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8') except: return "Errore Speedtest" # --- GESTIONE VIAGGI ATTIVI --- def load_viaggi_state() -> dict: """Carica lo stato dei viaggi attivi da file JSON""" if os.path.exists(VIAGGI_STATE_FILE): try: with open(VIAGGI_STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) or {} except Exception as e: logger.error(f"Errore lettura viaggi state: {e}") return {} return {} def save_viaggi_state(state: dict) -> None: """Salva lo stato dei viaggi attivi su file JSON""" try: with open(VIAGGI_STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"Errore scrittura viaggi state: {e}") def get_timezone_from_coords(lat: float, lon: float) -> str: """Ottiene la timezone da coordinate usando timezonefinder o fallback""" try: from timezonefinder import TimezoneFinder tf = TimezoneFinder() tz = tf.timezone_at(lng=lon, lat=lat) if tz: return tz except ImportError: logger.warning("timezonefinder non installato, uso fallback") except Exception as e: logger.warning(f"Errore timezonefinder: {e}") # Fallback: stima timezone da longitudine (approssimativo) # Ogni 15 gradi = 1 ora di differenza da UTC offset_hours = int(lon / 15) # Mappatura approssimativa a timezone IANA if -10 <= offset_hours <= 2: # Europa return "Europe/Rome" elif 3 <= offset_hours <= 5: # Medio Oriente return "Asia/Dubai" elif 6 <= offset_hours <= 8: # Asia centrale return "Asia/Kolkata" elif 9 <= offset_hours <= 11: # Asia orientale return "Asia/Tokyo" elif -5 <= offset_hours <= -3: # Americhe orientali return "America/New_York" elif -8 <= offset_hours <= -6: # Americhe occidentali return "America/Los_Angeles" else: return "UTC" def add_viaggio(chat_id: str, location: str, lat: float, lon: float, name: str, timezone: Optional[str] = None) -> None: """Aggiunge o aggiorna un viaggio attivo per un chat_id (sovrascrive se esiste)""" if timezone is None: timezone = get_timezone_from_coords(lat, lon) state = load_viaggi_state() state[chat_id] = { "location": location, "lat": lat, "lon": lon, "name": name, "timezone": timezone, "activated": datetime.datetime.now().isoformat() } save_viaggi_state(state) def remove_viaggio(chat_id: str) -> bool: """Rimuove un viaggio attivo per un chat_id. Ritorna True se rimosso, False se non esisteva""" state = load_viaggi_state() if chat_id in state: del state[chat_id] save_viaggi_state(state) return True return False def get_viaggio(chat_id: str) -> dict: """Ottiene il viaggio attivo per un chat_id, o None se non esiste""" state = load_viaggi_state() return state.get(chat_id) # --- HELPER PER LANCIARE SCRIPT ESTERNI --- def call_meteo_script(args_list): """Lancia meteo.py e cattura l'output testuale""" try: # Esegui: python3 meteo.py --arg1 val1 ... cmd = ["python3", METEO_SCRIPT] + args_list # Timeout aumentato a 90s per gestire retry e chiamate API multiple # (get_forecast può fare retry + fallback, get_visibility_forecast può fare 2 chiamate) result = subprocess.run(cmd, capture_output=True, text=True, timeout=90) return result.stdout if result.returncode == 0 else f"Errore Script: {result.stderr}" except subprocess.TimeoutExpired: return f"Errore esecuzione script: Timeout dopo 90 secondi (script troppo lento)" except Exception as e: return f"Errore esecuzione script: {e}" def call_meteo7_script(args_list): """Lancia previsione7.py e cattura l'output testuale""" try: # Esegui: python3 previsione7.py arg1 arg2 ... cmd = ["python3", METEO7_SCRIPT] + args_list result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) # previsione7.py invia direttamente a Telegram, quindi l'output potrebbe essere vuoto # Ritorniamo un messaggio di conferma se lo script è eseguito correttamente if result.returncode == 0: return "✅ Report previsione 7 giorni generato e inviato" else: return f"⚠️ Errore Script: {result.stderr[:500]}" except Exception as e: return f"⚠️ Errore esecuzione script: {e}" # --- HANDLERS BOT --- def restricted(func): @wraps(func) async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs): user_id = update.effective_user.id if user_id not in ALLOWED_IDS: return return await func(update, context, *args, **kwargs) return wrapped @restricted async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: keyboard = [ [InlineKeyboardButton("🖥️ Core Server", callback_data="menu_core"), InlineKeyboardButton("🔍 Scan LAN", callback_data="menu_lan")], [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 v8.1**\nComandi disponibili:\n🔹 `/meteo `\n🔹 `/meteo7 ` (Previsione 7gg)\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") @restricted async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) if not context.args: # Se non ci sono argomenti, controlla se c'è un viaggio attivo viaggio_attivo = get_viaggio(chat_id) if viaggio_attivo: # Invia report per Casa + località viaggio await update.message.reply_text( f"🔄 Generazione report meteo per Casa e {viaggio_attivo['name']}...", parse_mode="Markdown" ) # Report Casa report_casa = call_meteo_script(["--home"]) await update.message.reply_text( f"🏠 **Report Meteo - Casa**\n\n{report_casa}", parse_mode="Markdown" ) # Report località viaggio report_viaggio = call_meteo_script([ "--query", viaggio_attivo["location"], "--timezone", viaggio_attivo.get("timezone", "Europe/Rome") ]) await update.message.reply_text( f"✈️ **Report Meteo - {viaggio_attivo['name']}**\n\n{report_viaggio}", parse_mode="Markdown" ) else: # Nessun viaggio attivo: invia report per Casa await update.message.reply_text("🔄 Generazione report meteo per Casa...", parse_mode="Markdown") report_casa = call_meteo_script(["--home"]) await update.message.reply_text( f"🏠 **Report Meteo - Casa**\n\n{report_casa}", parse_mode="Markdown" ) return city = " ".join(context.args) await update.message.reply_text(f"🔄 Cerco '{city}'...", parse_mode="Markdown") # LANCIAMO LO SCRIPT ESTERNO! report = call_meteo_script(["--query", city]) await update.message.reply_text(report, parse_mode="Markdown") @restricted async def meteo7_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) if not context.args: # Se non ci sono argomenti, controlla se c'è un viaggio attivo viaggio_attivo = get_viaggio(chat_id) if viaggio_attivo: # Invia previsione 7gg per Casa + località viaggio await update.message.reply_text( f"📡 Calcolo previsione 7gg per Casa e {viaggio_attivo['name']}...", parse_mode="Markdown" ) # Previsione Casa subprocess.Popen([ "python3", METEO7_SCRIPT, "casa", "--chat_id", chat_id ]) # Previsione località viaggio subprocess.Popen([ "python3", METEO7_SCRIPT, viaggio_attivo["location"], "--chat_id", chat_id, "--timezone", viaggio_attivo.get("timezone", "Europe/Rome") ]) await update.message.reply_text( f"✅ Previsioni 7 giorni in arrivo per:\n" f"🏠 Casa\n" f"✈️ {viaggio_attivo['name']}", parse_mode="Markdown" ) else: # Nessun viaggio attivo, invia solo Casa await update.message.reply_text(f"📡 Calcolo previsione 7gg per Casa...", parse_mode="Markdown") subprocess.Popen(["python3", METEO7_SCRIPT, "casa", "--chat_id", chat_id]) return query = " ".join(context.args) await update.message.reply_text(f"📡 Calcolo previsione 7gg per: {query}...", parse_mode="Markdown") # Lancia in background subprocess.Popen(["python3", METEO7_SCRIPT, query, "--chat_id", chat_id]) @restricted async def snowradar_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Comando /snowradar: analisi neve in griglia 30km da San Marino""" chat_id = str(update.effective_chat.id) # Costruisci comando base # --debug: quando chiamato da Telegram, invia solo al chat_id richiedente # --chat_id: passa il chat_id specifico per inviare il messaggio cmd = ["python3", SNOW_RADAR_SCRIPT, "--debug", "--chat_id", chat_id] # Messaggio di avvio await update.message.reply_text( "❄️ **Snow Radar**\n\n" "Analisi neve in corso... Il report verrà inviato a breve.", parse_mode="Markdown" ) # Avvia in background subprocess.Popen(cmd, cwd=SCRIPT_DIR) @restricted async def irrigazione_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Comando /irrigazione: consulente agronomico per gestione irrigazione""" chat_id = str(update.effective_chat.id) # Costruisci comando base # --force: quando chiamato da Telegram, sempre invia (bypassa logica auto-reporting) cmd = ["python3", IRRIGATION_SCRIPT, "--telegram", "--chat_id", chat_id, "--force"] # Opzioni: --debug, o parametri posizionali per location if context.args: args_str = " ".join(context.args).lower() # Flag opzionali if "--debug" in args_str or "debug" in args_str: cmd.append("--debug") # Se ci sono altri argomenti non-flag, assumi siano per location remaining_args = [a for a in context.args if not a.startswith("--") and a.lower() not in ["debug", "force"]] if remaining_args: # Prova a interpretare come location (potrebbero essere coordinate o nome) location_str = " ".join(remaining_args) # Se sembra essere coordinate numeriche, usa --lat e --lon parts = location_str.split() if len(parts) == 2: try: lat = float(parts[0]) lon = float(parts[1]) cmd.extend(["--lat", str(lat), "--lon", str(lon)]) except ValueError: # Non sono numeri, probabilmente è un nome location cmd.extend(["--location", location_str]) else: cmd.extend(["--location", location_str]) # Messaggio di avvio await update.message.reply_text( "🌱 **Consulente Irrigazione**\n\n" "Analisi in corso... Il report verrà inviato a breve.", parse_mode="Markdown" ) # Esegui in background subprocess.Popen(cmd) @restricted async def road_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Comando /road: analizza tutti i rischi meteo lungo percorso stradale""" chat_id = str(update.effective_chat.id) if not context.args or len(context.args) < 2: await update.message.reply_text( "⚠️ **Uso:** `/road `\n\n" "Esempio: `/road Bologna Rimini`\n" "Esempio: `/road \"San Marino\" Rimini`\n" "Esempio: `/road \"San Marino di Castrozza\" \"San Martino di Castrozza\"`\n" "Usa virgolette per nomi con spazi multipli.\n\n" "Analizza tutti i rischi meteo lungo il percorso: ghiaccio, neve, pioggia, rovesci, nebbia, grandine, temporali.", parse_mode="Markdown" ) return # Parsing intelligente degli argomenti con supporto virgolette usando shlex def parse_quoted_args(args): """Parsa argomenti considerando virgolette per nomi multipli usando shlex.""" # Unisci tutti gli argomenti in una stringa e usa shlex per parsing corretto args_str = " ".join(args) try: # shlex.split gestisce correttamente virgolette singole e doppie parsed = shlex.split(args_str, posix=True) return parsed except ValueError: # Fallback: se shlex fallisce, usa metodo semplice result = [] current = [] in_quotes = False quote_char = None for arg in args: # Se inizia con virgolette, entra in modalità quote if arg.startswith('"') or arg.startswith("'"): in_quotes = True quote_char = arg[0] arg_clean = arg[1:] # Rimuovi virgolette iniziali current = [arg_clean] # Se finisce con virgolette, esci dalla modalità quote elif arg.endswith('"') or arg.endswith("'"): if in_quotes and (arg.endswith(quote_char) if quote_char else True): arg_clean = arg[:-1] # Rimuovi virgolette finali current.append(arg_clean) result.append(" ".join(current)) current = [] in_quotes = False quote_char = None else: result.append(arg) # Se siamo dentro le virgolette, aggiungi all'argomento corrente elif in_quotes: current.append(arg) # Altrimenti, argomento normale else: result.append(arg) # Se rimangono argomenti non chiusi, uniscili if current: result.append(" ".join(current)) return result parsed_args = parse_quoted_args(context.args) if len(parsed_args) < 2: await update.message.reply_text( "⚠️ Errore: servono almeno 2 località.\n" "Usa virgolette per nomi multipli: `/road \"San Marino\" Rimini`", parse_mode="Markdown" ) return city1 = parsed_args[0] city2 = parsed_args[1] await update.message.reply_text( f"🔄 Analisi rischi meteo stradali: {city1} → {city2}...", parse_mode="Markdown" ) try: # Importa funzioni da road_weather.py import sys sys.path.insert(0, SCRIPT_DIR) from road_weather import ( analyze_route_weather_risks, format_route_weather_report, generate_route_weather_map, PANDAS_AVAILABLE ) # Verifica disponibilità pandas if not PANDAS_AVAILABLE: await update.message.reply_text( "❌ **Errore: dipendenze mancanti**\n\n" "`pandas` e `numpy` sono richiesti per l'analisi avanzata.\n\n" "**Installazione nel container Docker:**\n" "```bash\n" "docker exec -it pip install --break-system-packages pandas numpy\n" "```\n\n" "Oppure aggiungi al Dockerfile:\n" "```dockerfile\n" "RUN pip install --break-system-packages pandas numpy\n" "```", parse_mode="Markdown" ) return # Analizza percorso (auto-detect del miglior modello disponibile per la zona) df = analyze_route_weather_risks(city1, city2, model_slug=None) if df is None or df.empty: await update.message.reply_text( f"❌ Errore: Impossibile ottenere dati per il percorso {city1} → {city2}.\n" f"Verifica che i nomi delle località siano corretti.", parse_mode="Markdown" ) return # Formatta e invia report (compatto, sempre in un singolo messaggio) report = format_route_weather_report(df, city1, city2) await update.message.reply_text(report, parse_mode="Markdown") # Genera e invia mappa del percorso (sempre, dopo il messaggio testuale) try: import tempfile map_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png', dir=SCRIPT_DIR) map_path = map_file.name map_file.close() map_generated = generate_route_weather_map(df, city1, city2, map_path) if map_generated: now = datetime.datetime.now() caption = ( f"🛣️ Mappa Rischi Meteo Stradali\n" f"📍 {city1} → {city2}\n" f"🕒 {now.strftime('%d/%m/%Y %H:%M')}" ) # Invia foto via Telegram with open(map_path, 'rb') as photo_file: await update.message.reply_photo( photo=photo_file, caption=caption, parse_mode="HTML" ) # Pulisci file temporaneo try: os.unlink(map_path) except: pass except Exception as map_error: logger.warning(f"Errore generazione mappa road: {map_error}") # Non bloccare l'esecuzione se la mappa fallisce except ImportError as e: # Gestione specifica per ImportError con messaggio dettagliato error_msg = str(e) await update.message.reply_text( f"❌ **Errore: dipendenze mancanti**\n\n{error_msg}", parse_mode="Markdown" ) except Exception as e: logger.error(f"Errore road_command: {e}", exc_info=True) await update.message.reply_text( f"❌ Errore durante l'analisi: {str(e)}", parse_mode="Markdown" ) @restricted async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Comando /meteo_viaggio: attiva/disattiva monitoraggio meteo per viaggio""" chat_id = str(update.effective_chat.id) # Gestione comando "fine" if context.args and len(context.args) == 1 and context.args[0].lower() in ["fine", "stop", "termina"]: viaggio_rimosso = remove_viaggio(chat_id) if viaggio_rimosso: await update.message.reply_text( "🎉 **Viaggio terminato!**\n\n" "✅ Il monitoraggio meteo personalizzato è stato disattivato.\n" "🏠 Ora riceverai solo gli avvisi per Casa.\n\n" "Bentornato a Casa! 👋", parse_mode="Markdown" ) else: await update.message.reply_text( "ℹ️ Nessun viaggio attivo da terminare.\n" "Usa `/meteo_viaggio ` per attivare un nuovo viaggio.", parse_mode="Markdown" ) return # Gestione attivazione viaggio if not context.args: viaggio_attivo = get_viaggio(chat_id) if viaggio_attivo: await update.message.reply_text( f"ℹ️ **Viaggio attivo**\n\n" f"📍 **{viaggio_attivo['name']}**\n" f"Attivato: {viaggio_attivo.get('activated', 'N/A')}\n\n" f"Usa `/meteo_viaggio fine` per terminare.", parse_mode="Markdown" ) else: await update.message.reply_text( "⚠️ Usa: `/meteo_viaggio `\n\n" "Esempio: `/meteo_viaggio Roma`\n\n" "Per terminare: `/meteo_viaggio fine`", parse_mode="Markdown" ) return location = " ".join(context.args) await update.message.reply_text(f"🔄 Attivazione monitoraggio viaggio per: **{location}**\n⏳ Elaborazione in corso...", parse_mode="Markdown") # Ottieni coordinate dalla localizzazione (usa meteo.py per geocoding) try: # Importa funzione get_coordinates da meteo.py import sys sys.path.insert(0, SCRIPT_DIR) from meteo import get_coordinates coords = get_coordinates(location) if not coords: await update.message.reply_text(f"❌ Località '{location}' non trovata. Verifica il nome e riprova.", parse_mode="Markdown") return lat, lon, name, cc = coords # Ottieni timezone per questa localizzazione timezone = get_timezone_from_coords(lat, lon) # Conferma riconoscimento località await update.message.reply_text( f"✅ **Località riconosciuta!**\n\n" f"📍 **{name}**\n" f"🌍 Coordinate: {lat:.4f}, {lon:.4f}\n" f"🕐 Fuso orario: {timezone}\n\n" f"⏳ Generazione report meteo in corso...", parse_mode="Markdown" ) # Salva viaggio attivo (sovrascrive se esiste già) add_viaggio(chat_id, location, lat, lon, name, timezone) # Esegui meteo.py in modo sincrono e invia output come conferma try: report_meteo = call_meteo_script([ "--query", location, "--timezone", timezone ]) if report_meteo and not report_meteo.startswith("Errore") and not report_meteo.startswith("⚠️"): # Invia report meteo come conferma await update.message.reply_text( f"📊 **Report Meteo - {name}**\n\n{report_meteo}", parse_mode="Markdown" ) else: await update.message.reply_text( f"⚠️ Errore nella generazione del report meteo:\n{report_meteo}", parse_mode="Markdown" ) except Exception as e: logger.exception(f"Errore esecuzione meteo.py: {e}") await update.message.reply_text( f"⚠️ Errore durante la generazione del report meteo: {str(e)}", parse_mode="Markdown" ) # Esegui previsione7.py (invia direttamente a Telegram) try: # Nota: previsione7.py invia direttamente a Telegram, quindi eseguiamo lo script result_meteo7 = subprocess.run( ["python3", METEO7_SCRIPT, location, "--chat_id", chat_id, "--timezone", timezone], capture_output=True, text=True, timeout=60 ) if result_meteo7.returncode == 0: await update.message.reply_text( f"✅ **Monitoraggio viaggio attivato!**\n\n" f"📨 **Report inviati:**\n" f"• Report meteo dettagliato ✓\n" f"• Previsione 7 giorni ✓\n\n" f"🎯 **Monitoraggio attivo per:**\n" f"📍 {name}\n" f"🕐 Fuso orario: {timezone}\n\n" f"📬 **Riceverai automaticamente:**\n" f"• Report meteo alle 8:00 AM (ora locale)\n" f"• Previsione 7 giorni alle 7:30 AM (ora locale)\n" f"• Avvisi meteo severi in tempo reale\n\n" f"Per terminare: `/meteo_viaggio fine`", parse_mode="Markdown" ) else: await update.message.reply_text( f"✅ Report meteo inviato!\n" f"⚠️ Errore nella previsione 7 giorni:\n{result_meteo7.stderr[:500]}\n\n" f"🎯 **Monitoraggio attivo per:** {name}", parse_mode="Markdown" ) except Exception as e: logger.exception(f"Errore esecuzione previsione7.py: {e}") await update.message.reply_text( f"✅ Report meteo inviato!\n" f"⚠️ Errore nella previsione 7 giorni: {str(e)}\n\n" f"🎯 **Monitoraggio attivo per:** {name}", parse_mode="Markdown" ) # Lancia severe_weather.py in background (non blocca la risposta) subprocess.Popen([ "python3", SEVERE_SCRIPT, "--lat", str(lat), "--lon", str(lon), "--location", name, "--timezone", timezone, "--chat_id", chat_id ]) except Exception as e: logger.exception("Errore in meteo_viaggio: %s", e) await update.message.reply_text(f"❌ Errore durante l'elaborazione: {str(e)}", parse_mode="Markdown") async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None: # Esegui una sola chiamata e invia il report a tutti i chat_id report = call_meteo_script(["--home"]) for uid in ALLOWED_IDS: try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown") except Exception: pass @restricted async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query await query.answer() data = query.data if data == "main_menu": await start(update, context) elif data == "req_meteo_home": await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown") # LANCIAMO LO SCRIPT ESTERNO report = call_meteo_script(["--home"]) keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "menu_core": keyboard = [] for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")]) keyboard.append([InlineKeyboardButton("📊 Report Completo", callback_data="stat_all")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]) await query.edit_message_text("🖥️ **Core Servers**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "stat_all": await query.edit_message_text("⏳ **Analisi...**", parse_mode="Markdown") report = "📊 **REPORT CORE**\n" for dev in CORE_DEVICES: report += f"\n🔹 **{dev['name']}**\n{get_device_stats(dev)}\n" await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") elif data.startswith("stat_"): dev = CORE_DEVICES[int(data.split("_")[1])] await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown") await query.edit_message_text(f"🔹 **{dev['name']}**\n\n{get_device_stats(dev)}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") elif data == "menu_lan": await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown") report = "🔍 **DIAGNOSTICA LAN**\n\n" try: for dev in CORE_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n" report += "\n" for dev in INFRA_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n" except Exception as e: report += f"\n⚠️ Errore: {e}" keyboard = [[InlineKeyboardButton("⚡ Menu Riavvio", callback_data="menu_reboot")], [InlineKeyboardButton("🔄 Aggiorna", callback_data="menu_lan"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "menu_reboot": keyboard = [] for i, dev in enumerate(INFRA_DEVICES): if "Router" not in dev['name']: keyboard.append([InlineKeyboardButton(f"⚡ {dev['name']}", callback_data=f"reboot_{i}")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="menu_lan")]) await query.edit_message_text("⚠️ **RIAVVIO REMOTO**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("reboot_"): dev = INFRA_DEVICES[int(data.split("_")[1])] res = run_cmd("reboot", dev['ip'], "admin") await query.edit_message_text(f"⚡ Inviato a {dev['name']}...\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown") elif data == "menu_pihole": status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER) icon = "✅" if "Enabled" in status_raw or "enabled" in status_raw else "🔴" text = f"🛡️ **Pi-hole Master**\nStato: {icon}\n\n`{status_raw}`" keyboard = [[InlineKeyboardButton("⏸️ 5m", callback_data="ph_disable_300"), InlineKeyboardButton("⏸️ 30m", callback_data="ph_disable_1800")], [InlineKeyboardButton("▶️ Attiva", callback_data="ph_enable"), InlineKeyboardButton("🔄 Restart", callback_data="ph_restart")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("ph_"): if "disable" in data: run_cmd(f"sudo pihole disable {data.split('_')[2]}s", MASTER_IP, SSH_USER) elif "enable" in data: run_cmd("sudo pihole enable", MASTER_IP, SSH_USER) elif "restart" in data: run_cmd("sudo systemctl restart pihole-FTL", MASTER_IP, SSH_USER) await query.edit_message_text("✅ Comando inviato.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_pihole")]]), parse_mode="Markdown") elif data == "menu_net": ip = run_cmd("curl -s ifconfig.me") keyboard = [[InlineKeyboardButton("🚀 Speedtest", callback_data="net_speedtest")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "net_speedtest": await query.edit_message_text("🚀 **Speedtest...**", parse_mode="Markdown") res = run_speedtest() await query.edit_message_text(f"🚀 **Risultato:**\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_net")]]), parse_mode="Markdown") elif data == "menu_logs": keyboard = [[InlineKeyboardButton("🐶 Watchdog", callback_data="log_wd"), InlineKeyboardButton("💾 Backup", callback_data="log_bk")], [InlineKeyboardButton("🔄 NPM Sync", callback_data="log_npm"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text("📜 **Logs**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("log_"): paths = {"log_wd": "/logs/dhcp-watchdog.log", "log_bk": "/logs/raspiBackup.log", "log_npm": "/logs/sync-npm.log"} log_c = read_log_file(paths[data]) 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 v8.1 (Modulare)...") application = Application.builder().token(BOT_TOKEN).build() application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("meteo", meteo_command)) application.add_handler(CommandHandler("meteo7", meteo7_command)) application.add_handler(CommandHandler("meteo_viaggio", meteo_viaggio_command)) application.add_handler(CommandHandler("road", road_command)) application.add_handler(CommandHandler("irrigazione", irrigazione_command)) application.add_handler(CommandHandler("snowradar", snowradar_command)) application.add_handler(CallbackQueryHandler(button_handler)) job_queue = application.job_queue job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=7, minute=15, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6)) application.run_polling() if __name__ == "__main__": main()