Backup automatico script del 2025-12-28 07:00

This commit is contained in:
2025-12-28 07:00:02 +01:00
parent 43d44e3e85
commit c89436c26b
3 changed files with 701 additions and 352 deletions

View File

@@ -1,22 +1,53 @@
import logging import logging
import subprocess import subprocess
import os import os
import datetime
import requests
from functools import wraps from functools import wraps
from zoneinfo import ZoneInfo
from dateutil import parser
from typing import Dict, List, Optional, Tuple
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ( from telegram.ext import (
Application, Application,
CommandHandler, CommandHandler,
CallbackQueryHandler, CallbackQueryHandler,
ContextTypes, ContextTypes,
JobQueue
) )
# =============================================================================
# LOOGLE BOT V7.0 (ULTIMATE)
# - Dashboard Sistema (SSH/Ping)
# - Meteo Arome ASCII (On-Demand + Schedulato)
# - Multi-User Security
# =============================================================================
# --- CONFIGURAZIONE --- # --- CONFIGURAZIONE ---
BOT_TOKEN = os.environ.get('BOT_TOKEN') BOT_TOKEN = os.environ.get('BOT_TOKEN')
OWNER_ID = int(os.environ.get('ALLOWED_USER_ID'))
# Gestione Multi-Utente
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()]
# Configurazione Sistema
SSH_USER = "daniely" SSH_USER = "daniely"
NAS_USER = "daniely" NAS_USER = "daniely"
MASTER_IP = "192.168.128.80" MASTER_IP = "192.168.128.80"
# Configurazione Meteo
HOME_LAT = 43.9356
HOME_LON = 12.4296
HOME_NAME = "🏠 Casa (Strada Cà Toro)"
TZ = "Europe/Rome"
TZINFO = ZoneInfo(TZ)
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
MODEL = "meteofrance_arome_france_hd"
HTTP_HEADERS = {"User-Agent": "loogle-bot-v7"}
# --- LISTE DISPOSITIVI --- # --- LISTE DISPOSITIVI ---
CORE_DEVICES = [ CORE_DEVICES = [
{"name": "🍓 Pi-1 (Master)", "ip": MASTER_IP, "type": "pi", "user": SSH_USER}, {"name": "🍓 Pi-1 (Master)", "ip": MASTER_IP, "type": "pi", "user": SSH_USER},
@@ -40,7 +71,9 @@ INFRA_DEVICES = [
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- FUNZIONI --- # =============================================================================
# SEZIONE 1: FUNZIONI SISTEMA (SSH, PING, UTILS)
# =============================================================================
def run_cmd(command, ip=None, user=None): def run_cmd(command, ip=None, user=None):
try: try:
@@ -53,24 +86,10 @@ def run_cmd(command, ip=None, user=None):
except Exception: return "Err" except Exception: return "Err"
def get_ping_icon(ip): def get_ping_icon(ip):
print(f"DEBUG: Pinging {ip}...") # LOG PER CAPIRE DOVE SI BLOCCA
try: try:
# Timeout aggressivo: 0.5 secondi (-W 1 è il minimo di ping standard, ma Python taglia a 0.8) subprocess.run(["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=0.8, check=True)
subprocess.run(
["ping", "-c", "1", "-W", "1", ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=0.8, # Timeout Python brutale
check=True
)
return "" return ""
except subprocess.TimeoutExpired: except Exception: return "🔴"
return "🔴" # Timeout Python
except subprocess.CalledProcessError:
return "🔴" # Risposta "Host Unreachable"
except Exception as e:
print(f"Errore Ping {ip}: {e}")
return ""
def get_device_stats(device): def get_device_stats(device):
ip, user, dtype = device['ip'], device['user'], device['type'] ip, user, dtype = device['ip'], device['user'], device['type']
@@ -90,10 +109,8 @@ def get_device_stats(device):
if dtype == "nas": ram_cmd = "free | grep Mem | awk '{printf \"%.0f%%\", $3*100/$2}'" 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%\"}'" 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_path = "/" if dtype != "nas" else "/volume1"
disk_cmd = f"df -h {disk_path} | awk 'NR==2{{print $5}}'" 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)}" 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): def read_log_file(filepath, lines=15):
@@ -105,11 +122,145 @@ def run_speedtest():
try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8') try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8')
except: return "Errore Speedtest" except: return "Errore Speedtest"
# --- BOT HANDLERS --- # =============================================================================
# SEZIONE 2: FUNZIONI METEO (AROME ASCII)
# =============================================================================
def now_local() -> datetime.datetime:
return datetime.datetime.now(TZINFO)
def parse_time(t: str) -> datetime.datetime:
dt = parser.isoparse(t)
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
return dt.astimezone(TZINFO)
def degrees_to_cardinal(d: int) -> str:
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
return dirs[round(d / 45) % 8]
def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape):
sky = "☁️"
if code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️"
elif (code in (45, 48) or vis < 1000) and prec < 1: sky = "🌫️"
elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️"
elif cloud <= 20: sky = "☀️" if is_day else "🌙"
elif cloud <= 40: sky = "🌤️" if is_day else "🌙"
elif cloud <= 60: sky = "⛅️"
elif cloud <= 80: sky = "🌥️"
sgx = "-"
if snow > 0 or (code in (71,73,75,77,85,86) if code else False): sgx = "☃️"
elif temp < 0 or (code in (66,67) if code else False): sgx = "🧊"
elif cape > 2000: sgx = "🌪️"
elif cape > 1000: sgx = ""
elif temp > 35: sgx = "🥵"
elif rain > 4: sgx = "☔️"
elif gust > 50: sgx = "💨"
return sky, sgx
def get_coordinates(city_name: str) -> Optional[Tuple[float, float, str]]:
params = {"name": city_name, "count": 1, "language": "it", "format": "json"}
try:
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
data = r.json()
if "results" in data and len(data["results"]) > 0:
res = data["results"][0]
name = f"{res.get('name')} ({res.get('country_code','')})"
return res["latitude"], res["longitude"], name
except Exception as e: logger.error(f"Geocoding error: {e}")
return None
def get_forecast(lat, lon) -> Optional[Dict]:
params = {
"latitude": lat, "longitude": lon, "timezone": TZ,
"forecast_days": 3, "models": MODEL,
"wind_speed_unit": "kmh", "precipitation_unit": "mm",
"hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility",
}
try:
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
r.raise_for_status()
return r.json()
except Exception as e: logger.error(f"Meteo API error: {e}"); return None
def generate_weather_report(lat, lon, location_name) -> str:
data = get_forecast(lat, lon)
if not data: return "❌ Errore API Meteo."
hourly = data.get("hourly", {})
times = hourly.get("time", [])
if not times: return "❌ Dati orari mancanti."
now = now_local().replace(minute=0, second=0, microsecond=0)
blocks = []
for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]:
end_time = now + datetime.timedelta(hours=hours_duration)
lines = [f"{'LT':<2} {'':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30]
count = 0
for i, t_str in enumerate(times):
try: dt = parse_time(t_str)
except: continue
if dt < now or dt >= end_time: continue
if dt.hour % step != 0: continue
try:
T = float(hourly["temperature_2m"][i])
Rh = int(hourly["relative_humidity_2m"][i] or 0)
Cl = int(hourly["cloudcover"][i] or 0)
Pr = float(hourly["precipitation"][i] or 0)
Rn = float(hourly["rain"][i] or 0)
Sn = float(hourly["snowfall"][i] or 0)
Wspd = float(hourly["windspeed_10m"][i] or 0)
Gust = float(hourly["windgusts_10m"][i] or 0)
Wdir = int(hourly["winddirection_10m"][i] or 0)
Cape = float(hourly["cape"][i] or 0)
Vis = float(hourly["visibility"][i] or 10000)
Code = int(hourly["weathercode"][i]) if hourly["weathercode"][i] is not None else None
IsDay = int(hourly["is_day"][i] if hourly["is_day"][i] is not None else 1)
t_s = f"{int(round(T))}"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}"
card = degrees_to_cardinal(Wdir)
w_val = Wspd
is_g = (Gust - Wspd) > 15
if is_g: w_val = Gust
w_txt = f"{card} {int(round(w_val))}"
if is_g:
g_txt = f"G{int(round(w_val))}"
if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}"
elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}"
else: w_txt = g_txt
w_fmt = f"{w_txt:<5}"
sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape)
lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}")
count += 1
except: continue
if count > 0:
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}"
blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```")
now = end_time
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n\n" + "\n\n".join(blocks)
# =============================================================================
# SEZIONE 3: BOT HANDLERS & SCHEDULER
# =============================================================================
# Decoratore Sicurezza Multi-Utente
def restricted(func): def restricted(func):
@wraps(func) @wraps(func)
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs): async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
if update.effective_user.id != OWNER_ID: return user_id = update.effective_user.id
if user_id not in ALLOWED_IDS:
logger.warning(f"⚠️ ACCESSO NEGATO: User {user_id}")
return
return await func(update, context, *args, **kwargs) return await func(update, context, *args, **kwargs)
return wrapped return wrapped
@@ -118,14 +269,40 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [ keyboard = [
[InlineKeyboardButton("🖥️ Core Server", callback_data="menu_core"), InlineKeyboardButton("🔍 Scan LAN", callback_data="menu_lan")], [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("🛡️ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")],
[InlineKeyboardButton("📜 Logs", callback_data="menu_logs")] [InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
] ]
text = "🎛 **Loogle Control Center v6.2**\nSeleziona un pannello:" text = "🎛 **Loogle Control Center v7.0**\nComandi disponibili:\n🔹 `/meteo <città>`\n🔹 Pulsanti sotto"
if update.message: if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
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:
if not context.args:
await update.message.reply_text("⚠️ Usa: `/meteo <città>` (es. `/meteo Rimini`)", parse_mode="Markdown")
return
city = " ".join(context.args)
await update.message.reply_text(f"🔄 Cerco '{city}'...", parse_mode="Markdown")
coords = get_coordinates(city)
if coords:
lat, lon, name = coords
report = generate_weather_report(lat, lon, name)
await update.message.reply_text(report, parse_mode="Markdown")
else: else:
await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown")
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
"""Funzione lanciata dallo scheduler alle 08:00"""
logger.info("⏰ Invio report automatico meteo...")
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME)
for uid in ALLOWED_IDS:
try:
await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
except Exception as e:
logger.error(f"Errore invio report a {uid}: {e}")
@restricted @restricted
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -135,15 +312,21 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
if data == "main_menu": await start(update, context) if data == "main_menu": await start(update, context)
elif data == "req_meteo_home":
await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown")
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME)
keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data == "menu_core": elif data == "menu_core":
keyboard = [] keyboard = []
for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")]) 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("📊 Report Completo", callback_data="stat_all")])
keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")])
await query.edit_message_text("🖥️ **Core Servers**\nDettagli CPU/RAM/Temp:", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") await query.edit_message_text("🖥️ **Core Servers**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data == "stat_all": elif data == "stat_all":
await query.edit_message_text("⏳ **Analisi Core Servers...**", parse_mode="Markdown") await query.edit_message_text("⏳ **Analisi...**", parse_mode="Markdown")
report = "📊 **REPORT CORE**\n" report = "📊 **REPORT CORE**\n"
for dev in CORE_DEVICES: report += f"\n🔹 **{dev['name']}**\n{get_device_stats(dev)}\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") await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown")
@@ -154,27 +337,14 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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") 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": elif data == "menu_lan":
await query.edit_message_text("⏳ **Scansione LAN rapida...**", parse_mode="Markdown") await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown")
report = "🔍 **DIAGNOSTICA LAN**\n\n" report = "🔍 **DIAGNOSTICA LAN**\n\n"
try: try:
# Core Devices for dev in CORE_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
for dev in CORE_DEVICES:
report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
report += "\n" report += "\n"
for dev in INFRA_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
# Infra Devices except Exception as e: report += f"\n⚠️ Errore: {e}"
for dev in INFRA_DEVICES: keyboard = [[InlineKeyboardButton("⚡ Menu Riavvio", callback_data="menu_reboot")], [InlineKeyboardButton("🔄 Aggiorna", callback_data="menu_lan"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
except Exception as e:
report += f"\n⚠️ Errore imprevisto durante scansione: {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") await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data == "menu_reboot": elif data == "menu_reboot":
@@ -182,14 +352,13 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
for i, dev in enumerate(INFRA_DEVICES): for i, dev in enumerate(INFRA_DEVICES):
if "Router" not in dev['name']: keyboard.append([InlineKeyboardButton(f"{dev['name']}", callback_data=f"reboot_{i}")]) 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")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="menu_lan")])
await query.edit_message_text("⚠️ **RIAVVIO REMOTO**\nFunziona solo se il dispositivo supporta SSH e hai le chiavi.", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") await query.edit_message_text("⚠️ **RIAVVIO REMOTO**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data.startswith("reboot_"): elif data.startswith("reboot_"):
dev = INFRA_DEVICES[int(data.split("_")[1])] dev = INFRA_DEVICES[int(data.split("_")[1])]
res = run_cmd("reboot", dev['ip'], "admin") res = run_cmd("reboot", dev['ip'], "admin")
await query.edit_message_text(f"Comando inviato a {dev['name']}...\n\nRisposta:\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown") 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")
# --- ALTRI MENU STANDARD ---
elif data == "menu_pihole": elif data == "menu_pihole":
status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER) status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER)
icon = "" if "Enabled" in status_raw or "enabled" in status_raw else "🔴" icon = "" if "Enabled" in status_raw or "enabled" in status_raw else "🔴"
@@ -209,7 +378,7 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data == "net_speedtest": elif data == "net_speedtest":
await query.edit_message_text("🚀 **Speedtest in corso...**", parse_mode="Markdown") await query.edit_message_text("🚀 **Speedtest...**", parse_mode="Markdown")
res = run_speedtest() 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") await query.edit_message_text(f"🚀 **Risultato:**\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_net")]]), parse_mode="Markdown")
@@ -223,9 +392,19 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown") await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown")
def main(): def main():
logger.info("Avvio Loogle Bot v7.0 (Ultimate)...")
application = Application.builder().token(BOT_TOKEN).build() application = Application.builder().token(BOT_TOKEN).build()
# Handlers
application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("meteo", meteo_command))
application.add_handler(CallbackQueryHandler(button_handler)) application.add_handler(CallbackQueryHandler(button_handler))
# SCHEDULER (Sostituisce CRON)
# Esegue il meteo tutti i giorni alle 08:00 Europe/Rome
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() application.run_polling()
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import datetime
import requests
import logging
from zoneinfo import ZoneInfo
from dateutil import parser
# --- CONFIGURAZIONE ---
# 1. Chiave API Voli (Tua)
RAPIDAPI_KEY = "841975fb1fmshd139bc1a12cd454p100114jsn8acf1ccede63"
# 2. Chat ID (Esclusivo per te)
CHAT_IDS = ["64463169"]
# 3. Lettura Token da file /etc
TOKEN_FILE = "/etc/telegram_dpc_bot_token"
def get_bot_token():
try:
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE, "r", encoding="utf-8") as f:
return f.read().strip()
except Exception as e:
print(f"Errore lettura file token {TOKEN_FILE}: {e}")
return None
BOT_TOKEN = get_bot_token()
TZ = ZoneInfo("Europe/Rome")
# --- FUNZIONI ---
def fetch_segment(iata, start_ts, end_ts):
"""Esegue la chiamata API per un segmento orario specifico"""
url = f"https://aerodatabox.p.rapidapi.com/flights/airports/iata/{iata}/{start_ts}/{end_ts}"
querystring = {
"withLeg": "true",
"direction": "Both",
"withCancelled": "false",
"withCodeshared": "false",
"withCargo": "false",
"withPrivate": "false"
}
headers = {
"X-RapidAPI-Key": RAPIDAPI_KEY,
"X-RapidAPI-Host": "aerodatabox.p.rapidapi.com"
}
try:
response = requests.get(url, headers=headers, params=querystring, timeout=15)
if response.status_code == 204: return [] # Nessun volo in questa fascia
if response.status_code != 200:
print(f"DEBUG {iata} Error {response.status_code}: {response.text}") # Debug approfondito
return None # Segnala errore
return response.json()
except Exception as e:
print(f"Exception fetching {iata}: {e}")
return None
def get_flights(iata, name):
"""Scarica arrivi e partenze dividendo la giornata in 2 blocchi da 12h"""
now = datetime.datetime.now(TZ)
date_str = now.strftime("%Y-%m-%d")
# AeroDataBox limita le richieste a finestre di 12 ore.
# Dividiamo in Mattina (00:00-11:59) e Pomeriggio (12:00-23:59)
segments = [
(f"{date_str}T00:00", f"{date_str}T11:59"),
(f"{date_str}T12:00", f"{date_str}T23:59")
]
raw_arrivals = []
raw_departures = []
error_occurred = False
for start_t, end_t in segments:
data = fetch_segment(iata, start_t, end_t)
if data is None:
error_occurred = True
continue
if isinstance(data, dict):
raw_arrivals.extend(data.get("arrivals", []))
raw_departures.extend(data.get("departures", []))
if error_occurred and not raw_arrivals and not raw_departures:
return f"\n⚠️ *{name} ({iata})* - Errore API (Vedi log)"
flight_list = []
# Processa Arrivi (Deduplica basata su orario+numero per sicurezza)
seen = set()
for f in raw_arrivals:
try:
time_local = f["movement"]["scheduledTimeLocal"]
dt = parser.isoparse(time_local)
flight_no = f.get("number", "")
uid = f"{dt}_{flight_no}_ARR"
if uid in seen: continue
seen.add(uid)
airline = f.get("airline", {}).get("name", "Unknown")
origin = f.get("movement", {}).get("airport", {}).get("name", "Unknown")
flight_list.append({
"time": dt,
"text": f"🛬 `{dt.strftime('%H:%M')}` da {origin}\n └ *{airline}* ({flight_no})"
})
except: continue
# Processa Partenze
for f in raw_departures:
try:
time_local = f["movement"]["scheduledTimeLocal"]
dt = parser.isoparse(time_local)
flight_no = f.get("number", "")
uid = f"{dt}_{flight_no}_DEP"
if uid in seen: continue
seen.add(uid)
airline = f.get("airline", {}).get("name", "Unknown")
dest = f.get("movement", {}).get("airport", {}).get("name", "Unknown")
flight_list.append({
"time": dt,
"text": f"🛫 `{dt.strftime('%H:%M')}` per {dest}\n └ *{airline}* ({flight_no})"
})
except: continue
if not flight_list:
return f"\n✈️ *{name} ({iata})*\n_Nessun volo programmato oggi._"
# Ordina per orario
flight_list.sort(key=lambda x: x["time"])
msg = f"\n✈️ *{name} ({iata})*"
for item in flight_list:
msg += f"\n{item['text']}"
return msg
def send_telegram(text):
if not BOT_TOKEN:
print(f"❌ ERRORE CRITICO: Token non trovato in {TOKEN_FILE}")
return
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
for chat_id in CHAT_IDS:
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown",
"disable_web_page_preview": True
}
try:
requests.post(url, json=payload, timeout=10)
print(f"Messaggio inviato a {chat_id}")
except Exception as e:
print(f"Errore invio a {chat_id}: {e}")
# --- MAIN ---
def main():
print("--- Elaborazione Voli (Split 12h) ---")
today = datetime.datetime.now(TZ).strftime("%d/%m/%Y")
report = f"📆 *PROGRAMMA VOLI {today}*\n"
report += get_flights("RMI", "Rimini")
report += "\n"
report += get_flights("FRL", "Forlì")
print(report)
send_telegram(report)
if __name__ == "__main__":
main()

View File

@@ -2,392 +2,373 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import html
import json import json
import logging import logging
import os import os
import time import time
import signal
import sys
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
# ============================================================================= # =============================================================================
# morning_weather_report.py # morning_weather_report.py (v3.3 - Full Bot Mode)
# #
# Report meteo giornaliero (08:00) per Casa (San Marino): # - Gira 24/7 (Daemon).
# - prossime 24h e 48h: Tmin/Tmax, nuvolosità, vento/raffiche, precipitazioni, fenomeni # - ORE 08:00: Invia report automatico Casa.
# # - COMANDI: Supporta "/meteo <città>" in chat.
# Telegram: # - BOTTONI: Supporta interazione click.
# - nessun token in chiaro (env oppure ~/.telegram_dpc_bot_token oppure /etc/telegram_dpc_bot_token) # - AUTO-KILL: Gestione processo unico.
# - invia SOLO il report (niente notifiche errori)
#
# Log:
# - ./morning_weather_report.log
# - DEBUG=1 -> anche su stdout
# ============================================================================= # =============================================================================
DEBUG = os.environ.get("DEBUG", "0").strip() == "1" DEBUG = os.environ.get("DEBUG", "0").strip() == "1"
# --- POSIZIONE CASA (Strada Cà Toro, 12 - San Marino) --- # --- POSIZIONE DEFAULT (CASA) ---
LAT = 43.9356 HOME_LAT = 43.9356
LON = 12.4296 HOME_LON = 12.4296
LOCATION_NAME = "🏠 Casa (Strada Cà Toro)" HOME_NAME = "🏠 Casa (Strada Cà Toro)"
# --- TIMEZONE --- # --- CONFIG ---
TZ = "Europe/Rome" TZ = "Europe/Rome"
TZINFO = ZoneInfo(TZ) TZINFO = ZoneInfo(TZ)
ADMIN_CHAT_ID = "64463169"
# --- TELEGRAM (multi-chat) ---
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"] TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token") TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token")
TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token" TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token"
PID_FILE = "/tmp/morning_weather_bot.pid"
# --- OPEN-METEO ---
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
MODEL = "meteofrance_arome_france_hd" MODEL = "meteofrance_arome_france_hd"
HTTP_HEADERS = {"User-Agent": "rpi-morning-weather-report/1.0"} HTTP_HEADERS = {"User-Agent": "rpi-morning-weather-report/3.3"}
# --- REPORT WINDOW --- # --- LOGGING ---
HOURS_1 = 24
HOURS_2 = 48 # includiamo fino a 48h, poi dividiamo 024 e 2448
# --- LOG FILE ---
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILE = os.path.join(BASE_DIR, "morning_weather_report.log") LOG_FILE = os.path.join(BASE_DIR, "morning_weather_report.log")
def setup_logger() -> logging.Logger: def setup_logger() -> logging.Logger:
logger = logging.getLogger("morning_weather_report") logger = logging.getLogger("morning_weather_report")
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
logger.handlers.clear() logger.handlers.clear()
fh = RotatingFileHandler(LOG_FILE, maxBytes=1_000_000, backupCount=5, encoding="utf-8") fh = RotatingFileHandler(LOG_FILE, maxBytes=1_000_000, backupCount=5, encoding="utf-8")
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s") fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
fh.setFormatter(fmt) fh.setFormatter(fmt)
logger.addHandler(fh) logger.addHandler(fh)
if DEBUG: if DEBUG:
sh = logging.StreamHandler() sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG) sh.setLevel(logging.DEBUG)
sh.setFormatter(fmt) sh.setFormatter(fmt)
logger.addHandler(sh) logger.addHandler(sh)
return logger return logger
LOGGER = setup_logger() LOGGER = setup_logger()
# --- GESTIONE PROCESSO UNICO ---
def kill_old_instance():
if os.path.exists(PID_FILE):
try:
with open(PID_FILE, "r") as f:
old_pid = int(f.read().strip())
if old_pid != os.getpid():
try:
os.kill(old_pid, signal.SIGTERM)
LOGGER.warning("💀 Uccisa vecchia istanza PID %s", old_pid)
time.sleep(1)
except ProcessLookupError: pass
except Exception as e: LOGGER.error("Errore kill PID: %s", e)
except Exception: pass
with open(PID_FILE, "w") as f:
f.write(str(os.getpid()))
# --- UTILS ---
def load_bot_token() -> str:
tok = (os.environ.get("TELEGRAM_BOT_TOKEN") or "").strip()
if tok: return tok
tok = (os.environ.get("BOT_TOKEN") or "").strip()
if tok: return tok
tok = _read_file(TOKEN_FILE_HOME)
if tok: return tok
tok = _read_file(TOKEN_FILE_ETC)
return tok.strip() if tok else ""
def _read_file(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f: return f.read().strip()
except: return ""
def get_token() -> str:
t = load_bot_token()
if not t: raise ValueError("Token Telegram mancante")
return t
def now_local() -> datetime.datetime: def now_local() -> datetime.datetime:
return datetime.datetime.now(TZINFO) return datetime.datetime.now(TZINFO)
def parse_time(t: str) -> datetime.datetime:
def read_text_file(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
return ""
except PermissionError:
LOGGER.debug("Permission denied reading %s", path)
return ""
except Exception as e:
LOGGER.exception("Error reading %s: %s", path, e)
return ""
def load_bot_token() -> str:
tok = (os.environ.get("TELEGRAM_BOT_TOKEN") or "").strip()
if tok:
return tok
tok = (os.environ.get("BOT_TOKEN") or "").strip()
if tok:
return tok
tok = read_text_file(TOKEN_FILE_HOME)
if tok:
return tok
tok = read_text_file(TOKEN_FILE_ETC)
return tok.strip() if tok else ""
def parse_time_to_local(t: str) -> datetime.datetime:
dt = parser.isoparse(t) dt = parser.isoparse(t)
if dt.tzinfo is None: if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
return dt.replace(tzinfo=TZINFO)
return dt.astimezone(TZINFO) return dt.astimezone(TZINFO)
# --- TELEGRAM API WRAPPER ---
def telegram_send_markdown(message: str) -> bool: def tg_send_message(chat_id: str, text: str, reply_markup: dict = None) -> Optional[dict]:
""" url = f"https://api.telegram.org/bot{get_token()}/sendMessage"
Invia il report. In caso di errori, SOLO log. payload = {
""" "chat_id": chat_id,
token = load_bot_token() "text": text,
if not token:
LOGGER.error("Telegram token missing: report NOT sent.")
return False
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload_base = {
"text": message,
"parse_mode": "Markdown", "parse_mode": "Markdown",
"disable_web_page_preview": True, "disable_web_page_preview": True
} }
if reply_markup: payload["reply_markup"] = reply_markup
ok_any = False
with requests.Session() as s:
for chat_id in TELEGRAM_CHAT_IDS:
payload = dict(payload_base)
payload["chat_id"] = chat_id
try: try:
r = s.post(url, json=payload, timeout=20) r = requests.post(url, json=payload, timeout=10)
if r.status_code == 200: return r.json() if r.status_code == 200 else None
ok_any = True
else:
LOGGER.error("Telegram HTTP %s chat_id=%s body=%s", r.status_code, chat_id, r.text[:500])
time.sleep(0.25)
except Exception as e: except Exception as e:
LOGGER.exception("Telegram send exception chat_id=%s err=%s", chat_id, e) LOGGER.error("Tg Send Error: %s", e)
return ok_any
# --- Weathercode categories (Open-Meteo / WMO-style codes) ---
def code_to_category(code: int) -> Optional[Tuple[str, str]]:
"""
Ritorna (emoji, label) per fenomeni "rilevanti" da elencare.
Non elenchiamo 'sereno/parzialmente nuvoloso' come fenomeno.
"""
if code is None:
return None return None
def tg_get_updates(offset: int = 0) -> List[dict]:
url = f"https://api.telegram.org/bot{get_token()}/getUpdates"
payload = {"offset": offset, "timeout": 30}
try: try:
c = int(code) r = requests.post(url, json=payload, timeout=35)
except Exception: if r.status_code == 200:
return r.json().get("result", [])
except Exception as e:
LOGGER.error("Tg Updates Error: %s", e)
time.sleep(2)
return []
def tg_answer_callback(callback_id: str, text: str = None):
url = f"https://api.telegram.org/bot{get_token()}/answerCallbackQuery"
payload = {"callback_query_id": callback_id}
if text: payload["text"] = text
try: requests.post(url, json=payload, timeout=5)
except: pass
# --- METEO & GEOCODING ---
def get_coordinates(city_name: str) -> Optional[Tuple[float, float, str]]:
params = {"name": city_name, "count": 1, "language": "it", "format": "json"}
try:
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
data = r.json()
if "results" in data and len(data["results"]) > 0:
res = data["results"][0]
name = f"{res.get('name')} ({res.get('country_code','')})"
return res["latitude"], res["longitude"], name
except Exception as e:
LOGGER.error("Geocoding error: %s", e)
return None return None
# Nebbia def get_forecast(lat, lon) -> Optional[Dict]:
if c in (45, 48):
return ("🌫️", "nebbia")
# Pioviggine
if 51 <= c <= 57:
return ("🌦️", "pioviggine")
# Pioggia / gelicidio
if 61 <= c <= 65:
return ("🌧️", "pioggia")
if c in (66, 67):
return ("🧊", "gelicidio")
# Neve
if 71 <= c <= 77:
return ("❄️", "neve")
# Rovesci
if 80 <= c <= 82:
return ("🌧️", "rovesci")
if c in (85, 86):
return ("❄️", "rovesci di neve")
# Temporali / grandine
if c == 95:
return ("⛈️", "temporali")
if c in (96, 99):
return ("⛈️", "temporali con grandine")
return None
def get_forecast() -> Optional[Dict]:
params = { params = {
"latitude": LAT, "latitude": lat, "longitude": lon, "timezone": TZ,
"longitude": LON, "forecast_days": 3, "models": MODEL,
"timezone": TZ, "wind_speed_unit": "kmh", "precipitation_unit": "mm",
"forecast_days": 3, # copre bene 48h "hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility",
"models": MODEL,
"wind_speed_unit": "kmh",
"precipitation_unit": "mm",
"hourly": ",".join([
"temperature_2m",
"cloudcover",
"windspeed_10m",
"windgusts_10m",
"precipitation",
"weathercode",
]),
} }
try: try:
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
if r.status_code == 400:
try:
j = r.json()
LOGGER.error("Open-Meteo 400: %s", j.get("reason", j))
except Exception:
LOGGER.error("Open-Meteo 400: %s", r.text[:500])
return None
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
except Exception as e: except Exception as e:
LOGGER.exception("Open-Meteo request error: %s", e) LOGGER.error("Meteo API Error: %s", e)
return None return None
# --- GENERAZIONE ASCII ---
def degrees_to_cardinal(d: int) -> str:
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
return dirs[round(d / 45) % 8]
def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape):
sky = "☁️"
if code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️"
elif (code in (45, 48) or vis < 1000) and prec < 1: sky = "🌫️"
elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️"
elif cloud <= 20: sky = "☀️" if is_day else "🌙"
elif cloud <= 40: sky = "🌤️" if is_day else "🌙"
elif cloud <= 60: sky = "⛅️"
elif cloud <= 80: sky = "🌥️"
sgx = "-"
if snow > 0 or (code in (71,73,75,77,85,86) if code else False): sgx = "☃️"
elif temp < 0 or (code in (66,67) if code else False): sgx = "🧊"
elif cape > 2000: sgx = "🌪️"
elif cape > 1000: sgx = ""
elif temp > 35: sgx = "🥵"
elif rain > 4: sgx = "☔️"
elif gust > 50: sgx = "💨"
return sky, sgx
def generate_report(lat, lon, location_name) -> str:
data = get_forecast(lat, lon)
if not data: return "❌ Errore scaricamento dati meteo."
hourly = data.get("hourly", {})
times = hourly.get("time", [])
if not times: return "❌ Dati orari mancanti."
now = now_local().replace(minute=0, second=0, microsecond=0)
blocks = []
for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]:
end_time = now + datetime.timedelta(hours=hours_duration)
lines = [f"{'LT':<2} {'':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30]
count = 0
for i, t_str in enumerate(times):
try: dt = parse_time(t_str)
except: continue
if dt < now or dt >= end_time: continue
if dt.hour % step != 0: continue
def window_indices(times: List[str], start: datetime.datetime, end: datetime.datetime) -> List[int]:
idx = []
for i, t in enumerate(times):
try: try:
dt = parse_time_to_local(t) T = float(hourly["temperature_2m"][i])
except Exception: Rh = int(hourly["relative_humidity_2m"][i] or 0)
continue Cl = int(hourly["cloudcover"][i] or 0)
if dt >= start and dt < end: Pr = float(hourly["precipitation"][i] or 0)
idx.append(i) Rn = float(hourly["rain"][i] or 0)
return idx Sn = float(hourly["snowfall"][i] or 0)
Wspd = float(hourly["windspeed_10m"][i] or 0)
Gust = float(hourly["windgusts_10m"][i] or 0)
Wdir = int(hourly["winddirection_10m"][i] or 0)
Cape = float(hourly["cape"][i] or 0)
Vis = float(hourly["visibility"][i] or 10000)
Code = int(hourly["weathercode"][i]) if hourly["weathercode"][i] is not None else None
IsDay = int(hourly["is_day"][i] if hourly["is_day"][i] is not None else 1)
t_s = f"{int(round(T))}"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}"
def summarize_window(hourly: Dict, idx: List[int]) -> Dict: card = degrees_to_cardinal(Wdir)
def safe_list(key: str) -> List[float]: w_val = Wspd
arr = hourly.get(key, []) or [] is_g = (Gust - Wspd) > 15
out = [] if is_g: w_val = Gust
for i in idx:
try:
v = arr[i]
out.append(float(v) if v is not None else 0.0)
except Exception:
out.append(0.0)
return out
temps = safe_list("temperature_2m") w_txt = f"{card} {int(round(w_val))}"
clouds = safe_list("cloudcover") if is_g:
wind = safe_list("windspeed_10m") g_txt = f"G{int(round(w_val))}"
gust = safe_list("windgusts_10m") if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}"
prec = safe_list("precipitation") elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}"
else: w_txt = g_txt
# weathercode: manteniamo int w_fmt = f"{w_txt:<5}"
wcodes_raw = (hourly.get("weathercode", []) or []) sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape)
wcodes = []
for i in idx:
try:
wcodes.append(int(wcodes_raw[i]))
except Exception:
wcodes.append(-1)
# Base stats lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}")
tmin = min(temps) if temps else None count += 1
tmax = max(temps) if temps else None except: continue
cavg = (sum(clouds) / len(clouds)) if clouds else None if count > 0:
cmax = max(clouds) if clouds else None day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}"
blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```")
now = end_time
wmax = max(wind) if wind else None return f"🌤️ *METEO REPORT*\n📍 {location_name}\n\n" + "\n\n".join(blocks)
gmax = max(gust) if gust else None
psum = sum(prec) if prec else 0.0 # --- MAIN LOOP ---
pmax = max(prec) if prec else 0.0
# Fenomeni (categorie uniche con prima occorrenza) def main():
phenomena: List[str] = [] kill_old_instance()
seen = set() LOGGER.info("--- Avvio Bot v3.3 (Full Daemon) ---")
for c in wcodes:
cat = code_to_category(c)
if not cat:
continue
em, label = cat
key = label
if key in seen:
continue
seen.add(key)
phenomena.append(f"{em} {label}")
# Se nessuna categoria ma piove un po' (precip) e code non classificato keyboard_main = {
if not phenomena and psum > 0.0: "inline_keyboard": [
phenomena.append("🌧️ precipitazioni") [
{"text": "🔎 Altra Località", "callback_data": "ask_city"},
return { {"text": "❌ Chiudi", "callback_data": "stop_bot"}
"tmin": tmin, "tmax": tmax, ]
"cavg": cavg, "cmax": cmax, ]
"wmax": wmax, "gmax": gmax,
"psum": psum, "pmax": pmax,
"phenomena": phenomena,
} }
offset = 0
user_states = {}
def fmt_num(v: Optional[float], dec: int = 1) -> str: # Variabile per evitare doppio invio nello stesso minuto
if v is None: last_report_date = None
return ""
try:
return f"{v:.{dec}f}"
except Exception:
return ""
def build_message(s0: Dict, s1: Dict, generated_at: datetime.datetime) -> str:
# Nota: Markdown. Manteniamo righe brevi.
dt_str = generated_at.strftime("%d/%m %H:%M")
def block(title: str, s: Dict) -> str:
phen = ", ".join(s["phenomena"]) if s["phenomena"] else ""
return (
f"*{title}*\n"
f"🌡️ Tmin/Tmax: `{fmt_num(s['tmin'])}°C` / `{fmt_num(s['tmax'])}°C`\n"
f"☁️ Nuvolosità: avg `{fmt_num(s['cavg'],0)}%` max `{fmt_num(s['cmax'],0)}%`\n"
f"💨 Vento/Raff.: `{fmt_num(s['wmax'],0)} km/h` / `{fmt_num(s['gmax'],0)} km/h`\n"
f"🌧️ Pioggia: `{fmt_num(s['psum'])} mm` (max/h `{fmt_num(s['pmax'])} mm`)\n"
f"🔎 Fenomeni: {phen}"
)
msg = (
f"🌤️ *REPORT METEO*\n"
f"📍 {LOCATION_NAME}\n"
f"🕗 Agg. `{dt_str}` (modello: `{MODEL}`)\n\n"
f"{block('024h', s0)}\n\n"
f"{block('2448h', s1)}\n"
)
return msg
def main() -> None:
LOGGER.info("--- Morning weather report ---")
data = get_forecast()
if not data:
# errori: solo log
return
hourly = data.get("hourly", {}) or {}
times = hourly.get("time", []) or []
if not times:
LOGGER.error("Open-Meteo: hourly.time missing/empty")
return
while True:
# 1. SCHEDULER INTERNO (Ore 08:00)
now = now_local() now = now_local()
start0 = now today_str = now.strftime("%Y-%m-%d")
end0 = now + datetime.timedelta(hours=HOURS_1)
start1 = end0
end1 = now + datetime.timedelta(hours=HOURS_2)
idx0 = window_indices(times, start0, end0) if now.hour == 8 and now.minute == 0 and last_report_date != today_str:
idx1 = window_indices(times, start1, end1) LOGGER.info("⏰ Invio report automatico delle 08:00")
report_home = generate_report(HOME_LAT, HOME_LON, HOME_NAME)
destinations = [ADMIN_CHAT_ID] if DEBUG else TELEGRAM_CHAT_IDS
for chat_id in destinations:
tg_send_message(chat_id, report_home, keyboard_main)
last_report_date = today_str
if not idx0 or not idx1: # 2. TELEGRAM POLLING
LOGGER.error("Insufficient hourly coverage for windows: idx0=%s idx1=%s", len(idx0), len(idx1)) updates = tg_get_updates(offset)
return
s0 = summarize_window(hourly, idx0) for u in updates:
s1 = summarize_window(hourly, idx1) offset = u["update_id"] + 1
msg = build_message(s0, s1, now) # --- Gestione Bottoni (Callback) ---
if "callback_query" in u:
cb = u["callback_query"]
cid = str(cb["from"]["id"])
cb_id = cb["id"]
data = cb.get("data")
ok = telegram_send_markdown(msg) if data == "stop_bot":
if ok: tg_answer_callback(cb_id, "Sessione chiusa")
LOGGER.info("Weather report sent.") tg_send_message(cid, "👋 Sessione meteo terminata.")
if cid in user_states: del user_states[cid]
elif data == "ask_city":
tg_answer_callback(cb_id, "Inserisci nome città")
tg_send_message(cid, "✍️ Scrivi il nome della città:")
user_states[cid] = "waiting_city_name"
# --- Gestione Messaggi Testo ---
elif "message" in u:
msg = u["message"]
cid = str(msg["chat"]["id"])
text = msg.get("text", "").strip()
# A) Gestione comando /meteo <città>
if text.lower().startswith("/meteo"):
query = text[6:].strip() # Rimuove "/meteo "
if query:
tg_send_message(cid, f"🔄 Cerco '{query}'...")
coords = get_coordinates(query)
if coords:
lat, lon, name = coords
LOGGER.info("CMD /meteo: User %s found %s", cid, name)
report = generate_report(lat, lon, name)
tg_send_message(cid, report, keyboard_main)
else: else:
LOGGER.error("Weather report NOT sent (token/telegram error).") tg_send_message(cid, f"❌ Città '{query}' non trovata.", keyboard_main)
else:
tg_send_message(cid, "⚠️ Usa: /meteo <nome città>\nEs: /meteo Rimini")
# B) Gestione attesa risposta dopo click bottone
elif cid in user_states and user_states[cid] == "waiting_city_name" and text:
tg_send_message(cid, f"🔄 Cerco '{text}'...")
coords = get_coordinates(text)
if coords:
lat, lon, name = coords
LOGGER.info("Button Flow: User %s found %s", cid, name)
report = generate_report(lat, lon, name)
tg_send_message(cid, report, keyboard_main)
else:
tg_send_message(cid, f"❌ Città '{text}' non trovata.", keyboard_main)
del user_states[cid]
time.sleep(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main()