Backup automatico script del 2025-12-28 07:00
This commit is contained in:
@@ -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} {'T°':>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")
|
||||||
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:
|
||||||
|
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:
|
||||||
@@ -134,16 +311,22 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
data = query.data
|
data = query.data
|
||||||
|
|
||||||
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__":
|
||||||
|
|||||||
189
services/telegram-bot/daily_flight_report.py
Normal file
189
services/telegram-bot/daily_flight_report.py
Normal 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()
|
||||||
@@ -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 0–24 e 24–48
|
|
||||||
|
|
||||||
# --- 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:
|
|
||||||
r = s.post(url, json=payload, timeout=20)
|
|
||||||
if r.status_code == 200:
|
|
||||||
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:
|
|
||||||
LOGGER.exception("Telegram send exception chat_id=%s err=%s", chat_id, 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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = int(code)
|
r = requests.post(url, json=payload, timeout=10)
|
||||||
except Exception:
|
return r.json() if r.status_code == 200 else None
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error("Tg Send Error: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Nebbia
|
def tg_get_updates(offset: int = 0) -> List[dict]:
|
||||||
if c in (45, 48):
|
url = f"https://api.telegram.org/bot{get_token()}/getUpdates"
|
||||||
return ("🌫️", "nebbia")
|
payload = {"offset": offset, "timeout": 30}
|
||||||
|
try:
|
||||||
|
r = requests.post(url, json=payload, timeout=35)
|
||||||
|
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 []
|
||||||
|
|
||||||
# Pioviggine
|
def tg_answer_callback(callback_id: str, text: str = None):
|
||||||
if 51 <= c <= 57:
|
url = f"https://api.telegram.org/bot{get_token()}/answerCallbackQuery"
|
||||||
return ("🌦️", "pioviggine")
|
payload = {"callback_query_id": callback_id}
|
||||||
|
if text: payload["text"] = text
|
||||||
|
try: requests.post(url, json=payload, timeout=5)
|
||||||
|
except: pass
|
||||||
|
|
||||||
# Pioggia / gelicidio
|
# --- METEO & GEOCODING ---
|
||||||
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")
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_forecast(lat, lon) -> Optional[Dict]:
|
||||||
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 window_indices(times: List[str], start: datetime.datetime, end: datetime.datetime) -> List[int]:
|
def degrees_to_cardinal(d: int) -> str:
|
||||||
idx = []
|
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||||
for i, t in enumerate(times):
|
return dirs[round(d / 45) % 8]
|
||||||
try:
|
|
||||||
dt = parse_time_to_local(t)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if dt >= start and dt < end:
|
|
||||||
idx.append(i)
|
|
||||||
return idx
|
|
||||||
|
|
||||||
|
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 summarize_window(hourly: Dict, idx: List[int]) -> Dict:
|
def generate_report(lat, lon, location_name) -> str:
|
||||||
def safe_list(key: str) -> List[float]:
|
data = get_forecast(lat, lon)
|
||||||
arr = hourly.get(key, []) or []
|
if not data: return "❌ Errore scaricamento dati meteo."
|
||||||
out = []
|
|
||||||
for i in idx:
|
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} {'T°':>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:
|
try:
|
||||||
v = arr[i]
|
T = float(hourly["temperature_2m"][i])
|
||||||
out.append(float(v) if v is not None else 0.0)
|
Rh = int(hourly["relative_humidity_2m"][i] or 0)
|
||||||
except Exception:
|
Cl = int(hourly["cloudcover"][i] or 0)
|
||||||
out.append(0.0)
|
Pr = float(hourly["precipitation"][i] or 0)
|
||||||
return out
|
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
|
||||||
|
|
||||||
temps = safe_list("temperature_2m")
|
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n\n" + "\n\n".join(blocks)
|
||||||
clouds = safe_list("cloudcover")
|
|
||||||
wind = safe_list("windspeed_10m")
|
|
||||||
gust = safe_list("windgusts_10m")
|
|
||||||
prec = safe_list("precipitation")
|
|
||||||
|
|
||||||
# weathercode: manteniamo int
|
# --- MAIN LOOP ---
|
||||||
wcodes_raw = (hourly.get("weathercode", []) or [])
|
|
||||||
wcodes = []
|
|
||||||
for i in idx:
|
|
||||||
try:
|
|
||||||
wcodes.append(int(wcodes_raw[i]))
|
|
||||||
except Exception:
|
|
||||||
wcodes.append(-1)
|
|
||||||
|
|
||||||
# Base stats
|
def main():
|
||||||
tmin = min(temps) if temps else None
|
kill_old_instance()
|
||||||
tmax = max(temps) if temps else None
|
LOGGER.info("--- Avvio Bot v3.3 (Full Daemon) ---")
|
||||||
|
|
||||||
cavg = (sum(clouds) / len(clouds)) if clouds else None
|
keyboard_main = {
|
||||||
cmax = max(clouds) if clouds else None
|
"inline_keyboard": [
|
||||||
|
[
|
||||||
wmax = max(wind) if wind else None
|
{"text": "🔎 Altra Località", "callback_data": "ask_city"},
|
||||||
gmax = max(gust) if gust else None
|
{"text": "❌ Chiudi", "callback_data": "stop_bot"}
|
||||||
|
]
|
||||||
psum = sum(prec) if prec else 0.0
|
]
|
||||||
pmax = max(prec) if prec else 0.0
|
|
||||||
|
|
||||||
# Fenomeni (categorie uniche con prima occorrenza)
|
|
||||||
phenomena: List[str] = []
|
|
||||||
seen = set()
|
|
||||||
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
|
|
||||||
if not phenomena and psum > 0.0:
|
|
||||||
phenomena.append("🌧️ precipitazioni")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"tmin": tmin, "tmax": tmax,
|
|
||||||
"cavg": cavg, "cmax": cmax,
|
|
||||||
"wmax": wmax, "gmax": gmax,
|
|
||||||
"psum": psum, "pmax": pmax,
|
|
||||||
"phenomena": phenomena,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
user_states = {}
|
||||||
|
|
||||||
|
# Variabile per evitare doppio invio nello stesso minuto
|
||||||
|
last_report_date = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# 1. SCHEDULER INTERNO (Ore 08:00)
|
||||||
|
now = now_local()
|
||||||
|
today_str = now.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
if now.hour == 8 and now.minute == 0 and last_report_date != today_str:
|
||||||
|
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
|
||||||
|
|
||||||
def fmt_num(v: Optional[float], dec: int = 1) -> str:
|
# 2. TELEGRAM POLLING
|
||||||
if v is None:
|
updates = tg_get_updates(offset)
|
||||||
return "—"
|
|
||||||
try:
|
for u in updates:
|
||||||
return f"{v:.{dec}f}"
|
offset = u["update_id"] + 1
|
||||||
except Exception:
|
|
||||||
return "—"
|
# --- 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")
|
||||||
|
|
||||||
|
if data == "stop_bot":
|
||||||
|
tg_answer_callback(cb_id, "Sessione chiusa")
|
||||||
|
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:
|
||||||
|
tg_send_message(cid, f"❌ Città '{query}' non trovata.", keyboard_main)
|
||||||
|
else:
|
||||||
|
tg_send_message(cid, "⚠️ Usa: /meteo <nome città>\nEs: /meteo Rimini")
|
||||||
|
|
||||||
def build_message(s0: Dict, s1: Dict, generated_at: datetime.datetime) -> str:
|
# B) Gestione attesa risposta dopo click bottone
|
||||||
# Nota: Markdown. Manteniamo righe brevi.
|
elif cid in user_states and user_states[cid] == "waiting_city_name" and text:
|
||||||
dt_str = generated_at.strftime("%d/%m %H:%M")
|
tg_send_message(cid, f"🔄 Cerco '{text}'...")
|
||||||
|
coords = get_coordinates(text)
|
||||||
def block(title: str, s: Dict) -> str:
|
if coords:
|
||||||
phen = ", ".join(s["phenomena"]) if s["phenomena"] else "—"
|
lat, lon, name = coords
|
||||||
return (
|
LOGGER.info("Button Flow: User %s found %s", cid, name)
|
||||||
f"*{title}*\n"
|
report = generate_report(lat, lon, name)
|
||||||
f"🌡️ Tmin/Tmax: `{fmt_num(s['tmin'])}°C` / `{fmt_num(s['tmax'])}°C`\n"
|
tg_send_message(cid, report, keyboard_main)
|
||||||
f"☁️ Nuvolosità: avg `{fmt_num(s['cavg'],0)}%` max `{fmt_num(s['cmax'],0)}%`\n"
|
else:
|
||||||
f"💨 Vento/Raff.: `{fmt_num(s['wmax'],0)} km/h` / `{fmt_num(s['gmax'],0)} km/h`\n"
|
tg_send_message(cid, f"❌ Città '{text}' non trovata.", keyboard_main)
|
||||||
f"🌧️ Pioggia: `{fmt_num(s['psum'])} mm` (max/h `{fmt_num(s['pmax'])} mm`)\n"
|
|
||||||
f"🔎 Fenomeni: {phen}"
|
del user_states[cid]
|
||||||
)
|
|
||||||
|
time.sleep(1)
|
||||||
msg = (
|
|
||||||
f"🌤️ *REPORT METEO*\n"
|
|
||||||
f"📍 {LOCATION_NAME}\n"
|
|
||||||
f"🕗 Agg. `{dt_str}` (modello: `{MODEL}`)\n\n"
|
|
||||||
f"{block('0–24h', s0)}\n\n"
|
|
||||||
f"{block('24–48h', 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
|
|
||||||
|
|
||||||
now = now_local()
|
|
||||||
start0 = now
|
|
||||||
end0 = now + datetime.timedelta(hours=HOURS_1)
|
|
||||||
start1 = end0
|
|
||||||
end1 = now + datetime.timedelta(hours=HOURS_2)
|
|
||||||
|
|
||||||
idx0 = window_indices(times, start0, end0)
|
|
||||||
idx1 = window_indices(times, start1, end1)
|
|
||||||
|
|
||||||
if not idx0 or not idx1:
|
|
||||||
LOGGER.error("Insufficient hourly coverage for windows: idx0=%s idx1=%s", len(idx0), len(idx1))
|
|
||||||
return
|
|
||||||
|
|
||||||
s0 = summarize_window(hourly, idx0)
|
|
||||||
s1 = summarize_window(hourly, idx1)
|
|
||||||
|
|
||||||
msg = build_message(s0, s1, now)
|
|
||||||
|
|
||||||
ok = telegram_send_markdown(msg)
|
|
||||||
if ok:
|
|
||||||
LOGGER.info("Weather report sent.")
|
|
||||||
else:
|
|
||||||
LOGGER.error("Weather report NOT sent (token/telegram error).")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user