Backup automatico script del 2026-01-11 07:00

This commit is contained in:
2026-01-11 07:00:03 +01:00
parent 2859b95dbc
commit 4555d6615e
20 changed files with 13373 additions and 887 deletions

776
services/telegram-bot/bot.py Executable file → Normal file
View File

@@ -3,7 +3,10 @@ import subprocess
import os
import datetime
import requests
import shlex
import json
from functools import wraps
from typing import Optional
from zoneinfo import ZoneInfo
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
@@ -15,13 +18,10 @@ from telegram.ext import (
)
# =============================================================================
# LOOGLE BOT V9.0 (ULTIMATE + CAMERAS + MODULAR)
# - Dashboard Sistema (SSH/WOL/Monitor)
# - Meteo Smart (Meteo.py / Previsione7.py)
# - CCTV Hub (Cam.py + FFMPEG)
# LOOGLE BOT V8.1 (MODULARE + ON-DEMAND METEO)
# =============================================================================
# --- CONFIGURAZIONE AMBIENTE ---
# --- 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()]
@@ -32,20 +32,25 @@ MASTER_IP = "192.168.128.80"
TZ = "Europe/Rome"
TZINFO = ZoneInfo(TZ)
# --- GESTIONE PERCORSI DINAMICA (DOCKER FRIENDLY) ---
# PERCORSI SCRIPT
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
METEO_SCRIPT = os.path.join(SCRIPT_DIR, "meteo.py")
METEO_SCRIPT = os.path.join(SCRIPT_DIR, "meteo.py") # SCRIPT METEO SEPARATO
METEO7_SCRIPT = os.path.join(SCRIPT_DIR, "previsione7.py")
CAM_SCRIPT = os.path.join(SCRIPT_DIR, "cam.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")
# --- LISTE DISPOSITIVI ---
# 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"},
@@ -58,19 +63,14 @@ INFRA_DEVICES = [
{"name": "🔌 Sw Tav", "ip": "192.168.128.108"}
]
# Configurazione Logging
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
# =============================================================================
# SEZIONE 1: FUNZIONI UTILI E HELPER
# =============================================================================
# --- FUNZIONI SISTEMA (SSH/PING) ---
def run_cmd(command, ip=None, user=None):
"""Esegue comandi shell locali o via SSH"""
try:
if ip == "127.0.0.1" or ip is None:
return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip()
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}'"
@@ -84,13 +84,11 @@ def get_ping_icon(ip):
except Exception: return "🔴"
def get_device_stats(device):
ip, user, dtype = device['ip'], device['user'], device['type']
ip, user, dtype = device['ip'], device['type'], device['user']
uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user)
if not uptime_raw or "Err" in uptime_raw: 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"
@@ -98,7 +96,6 @@ def get_device_stats(device):
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"
@@ -111,27 +108,124 @@ def read_log_file(filepath, lines=15):
except Exception as e: return f"Errore: {str(e)}"
def run_speedtest():
try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=60).decode('utf-8')
try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8')
except: return "Errore Speedtest"
def call_script_text(script_path, args_list):
"""Wrapper per lanciare script che restituiscono testo (Meteo)"""
# --- 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:
cmd = ["python3", script_path] + args_list
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
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return result.stdout.strip() if result.returncode == 0 else f"⚠️ Errore Script:\n{result.stderr}"
except Exception as e: return f"❌ Errore esecuzione: {e}"
return result.stdout if result.returncode == 0 else f"Errore Script: {result.stderr}"
except Exception as e:
return f"Errore esecuzione script: {e}"
# =============================================================================
# SEZIONE 2: GESTORI COMANDI (HANDLERS)
# =============================================================================
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}"
# Decoratore Sicurezza
# --- 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
if user_id not in ALLOWED_IDS:
return
return await func(update, context, *args, **kwargs)
return wrapped
@@ -140,161 +234,522 @@ 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("📹 Camere", callback_data="menu_cams")],
[InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
[InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
]
text = "🎛 **Loogle Control Center v9.0**\n\n🔹 `/meteo <città>`\n🔹 `/meteo7 <città>` (7 Giorni)\n🔹 `/cam <nome>` (Snapshot)"
text = "🎛 **Loogle Control Center v8.1**\nComandi disponibili:\n🔹 `/meteo <città>`\n🔹 `/meteo7 <città>` (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:
query = " ".join(context.args).strip()
if not query or query.lower() == "casa":
await update.message.reply_text("⏳ **Scarico Meteo Casa...**", parse_mode="Markdown")
report = call_script_text(METEO_SCRIPT, ["--home"])
else:
await update.message.reply_text(f"🔄 Cerco '{query}'...", parse_mode="Markdown")
report = call_script_text(METEO_SCRIPT, ["--query", query])
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 = update.effective_chat.id
query = "casa"
if context.args: query = " ".join(context.args)
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")
subprocess.Popen(["python3", METEO7_SCRIPT, query, "--chat_id", str(chat_id)])
# Lancia in background
subprocess.Popen(["python3", METEO7_SCRIPT, query, "--chat_id", chat_id])
@restricted
async def cam_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not context.args:
# Se non c'è argomento, mostra il menu camere
keyboard = [
[InlineKeyboardButton("📷 Sala", callback_data="req_cam_sala"), InlineKeyboardButton("📷 Ingresso", callback_data="req_cam_ingresso")],
[InlineKeyboardButton("📷 Taverna", callback_data="req_cam_taverna"), InlineKeyboardButton("📷 Retro", callback_data="req_cam_retro")],
[InlineKeyboardButton("📷 Matrim.", callback_data="req_cam_matrimoniale"), InlineKeyboardButton("📷 Luca", callback_data="req_cam_luca")],
[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]
]
await update.message.reply_text("📹 **Scegli una telecamera:**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
return
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)
cam_name = context.args[0]
await update.message.reply_chat_action(action="upload_photo")
@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 <località1> <località2>`\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:
# Timeout 15s per RTSP
result = subprocess.run(["python3", CAM_SCRIPT, cam_name], capture_output=True, text=True, timeout=15)
output = result.stdout.strip()
# 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
)
if output.startswith("OK:"):
img_path = output.split(":", 1)[1]
await update.message.reply_photo(photo=open(img_path, 'rb'), caption=f"📷 **{cam_name.capitalize()}**")
elif output.startswith("ERR:"):
await update.message.reply_text(output.split(":", 1)[1])
else:
await update.message.reply_text(f"❌ Risposta imprevista dallo script: {output}")
# 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 <container_name> 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"🛣️ <b>Mappa Rischi Meteo Stradali</b>\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:
await update.message.reply_text(f"Errore critico: {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 <località>` 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 <località>`\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:
# Meteo automatico alle 8:00
report = call_script_text(METEO_SCRIPT, ["--home"])
# LANCIAMO LO SCRIPT ESTERNO PER CASA
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: pass
@restricted
async def clip_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not context.args:
await update.message.reply_text("⚠️ Usa: `/clip <nome_camera>` (es. /clip sala)", parse_mode="Markdown")
return
cam_name = context.args[0]
await update.message.reply_chat_action(action="upload_video") # Icona "sta inviando video..."
await update.message.reply_text(f"🎥 **Registro 10s da {cam_name}...**", parse_mode="Markdown")
try:
# Lancia lo script con flag --video
result = subprocess.run(["python3", CAM_SCRIPT, cam_name, "--video"], capture_output=True, text=True, timeout=20)
output = result.stdout.strip()
if output.startswith("OK:"):
vid_path = output.split(":", 1)[1]
await update.message.reply_video(video=open(vid_path, 'rb'), caption=f"🎥 **Clip: {cam_name.capitalize()}**")
elif output.startswith("ERR:"):
await update.message.reply_text(output.split(":", 1)[1])
except Exception as e:
await update.message.reply_text(f"❌ Errore critico: {e}")
@restricted
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer() # Risposta immediata per togliere il loading dal pulsante
await query.answer()
data = query.data
# --- NAVIGAZIONE MENU ---
if data == "main_menu":
await start(update, context)
if data == "main_menu": await start(update, context)
# --- SEZIONE METEO ---
elif data == "req_meteo_home":
await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown")
report = call_script_text(METEO_SCRIPT, ["--home"])
# 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")
# --- SEZIONE CAMERE ---
elif data == "menu_cams":
keyboard = [
[InlineKeyboardButton("📷 Sala", callback_data="req_cam_sala"), InlineKeyboardButton("📷 Ingresso", callback_data="req_cam_ingresso")],
[InlineKeyboardButton("📷 Taverna", callback_data="req_cam_taverna"), InlineKeyboardButton("📷 Retro", callback_data="req_cam_retro")],
[InlineKeyboardButton("📷 Matrim.", callback_data="req_cam_matrimoniale"), InlineKeyboardButton("📷 Luca", callback_data="req_cam_luca")],
[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]
]
await query.edit_message_text("📹 **Centrale Video**\nSeleziona una telecamera:", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data.startswith("req_cam_"):
cam_name = data.replace("req_cam_", "")
# Non editiamo il messaggio, inviamo una nuova foto sotto
try:
result = subprocess.run(["python3", CAM_SCRIPT, cam_name], capture_output=True, text=True, timeout=15)
output = result.stdout.strip()
if output.startswith("OK:"):
img_path = output.split(":", 1)[1]
await context.bot.send_photo(chat_id=update.effective_chat.id, photo=open(img_path, 'rb'), caption=f"📷 **{cam_name.capitalize()}**")
elif output.startswith("ERR:"):
await context.bot.send_message(chat_id=update.effective_chat.id, text=output.split(":", 1)[1])
except Exception as e:
await context.bot.send_message(chat_id=update.effective_chat.id, text=f"❌ Errore richiesta cam: {e}")
elif data.startswith("req_vid_"):
cam_name = data.replace("req_vid_", "")
await query.answer("🎥 Registrazione in corso (10s)...")
# Inviamo un messaggio di attesa perché ci mette un po'
msg = await context.bot.send_message(chat_id=update.effective_chat.id, text=f"⏳ Registro clip: {cam_name}...")
try:
result = subprocess.run(["python3", CAM_SCRIPT, cam_name, "--video"], capture_output=True, text=True, timeout=20)
output = result.stdout.strip()
# Cancelliamo il messaggio di attesa
await context.bot.delete_message(chat_id=update.effective_chat.id, message_id=msg.message_id)
if output.startswith("OK:"):
vid_path = output.split(":", 1)[1]
await context.bot.send_video(chat_id=update.effective_chat.id, video=open(vid_path, 'rb'), caption=f"🎥 **Clip: {cam_name.capitalize()}**")
elif output.startswith("ERR:"):
await context.bot.send_message(chat_id=update.effective_chat.id, text=output.split(":", 1)[1])
except Exception as e:
await context.bot.send_message(chat_id=update.effective_chat.id, text=f"❌ Errore: {e}")
# --- SEZIONE SISTEMA CORE ---
elif data == "menu_core":
keyboard = []
for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")])
@@ -313,7 +768,6 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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")
# --- SEZIONE LAN ---
elif data == "menu_lan":
await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown")
report = "🔍 **DIAGNOSTICA LAN**\n\n"
@@ -337,7 +791,6 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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")
# --- SEZIONE PI-HOLE ---
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 "🔴"
@@ -351,18 +804,16 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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")
# --- SEZIONE RETE ---
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... (attendi 40s)**", parse_mode="Markdown")
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")
# --- SEZIONE LOGS ---
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")
@@ -372,25 +823,24 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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 v9.0 (Modular)...")
logger.info("Avvio Loogle Bot v8.1 (Modulare)...")
application = Application.builder().token(BOT_TOKEN).build()
# Registrazione Comandi
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("meteo", meteo_command))
application.add_handler(CommandHandler("meteo7", meteo7_command))
application.add_handler(CommandHandler("cam", cam_command))
application.add_handler(CommandHandler("clip", clip_command))
# Registrazione Callback Menu
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))
# 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))
application.run_polling()
if __name__ == "__main__":
main()
main()