Backup automatico script del 2026-01-11 07:00
This commit is contained in:
776
services/telegram-bot/bot.py
Executable file → Normal file
776
services/telegram-bot/bot.py
Executable file → Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user