Compare commits

..

9 Commits

6 changed files with 705 additions and 85 deletions

View File

@@ -1,26 +1,61 @@
import requests import requests
import datetime import datetime
import time import time
import json
import os
from dateutil import parser from dateutil import parser
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
# --- CONFIGURAZIONE UTENTE --- # --- CONFIGURAZIONE UTENTE ---
# 👇👇 INSERISCI QUI I TUOI DATI 👇👇 # 👇👇 INSERISCI QUI I TUOI DATI 👇👇
TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4" TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
TELEGRAM_CHAT_IDS = ["64463169", "132455422"] TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
# --- PUNTI DI MONITORAGGIO --- # --- PUNTI DI MONITORAGGIO ---
# Il primo punto DEVE essere Casa tua # Sostituito San Leo con Carpegna
POINTS = [ POINTS = [
{"name": "🏠 Casa", "lat": 43.9356, "lon": 12.4296}, {"name": "🏠 Casa", "lat": 43.9356, "lon": 12.4296},
{"name": "⛰️ Titano", "lat": 43.9360, "lon": 12.4460}, {"name": "⛰️ Titano", "lat": 43.9360, "lon": 12.4460},
{"name": "🏢 Dogana", "lat": 43.9800, "lon": 12.4900}, {"name": "🏢 Dogana", "lat": 43.9800, "lon": 12.4900},
{"name": "🏰 San Leo", "lat": 43.8900, "lon": 12.3400} {"name": "🏔️ Carpegna", "lat": 43.7819, "lon": 12.3346}
] ]
# Soglia notifica (cm) # Soglia notifica (cm)
SOGLIA_NOTIFICA = 0.0 SOGLIA_NOTIFICA = 0.0
# File di stato per ricordare l'ultima allerta
STATE_FILE = "/home/daniely/docker/telegram-bot/snow_state.json"
# --- FUNZIONI DI UTILITÀ ---
def is_winter_season():
"""Ritorna True se oggi è tra il 1 Novembre e il 15 Aprile"""
now = datetime.datetime.now()
month = now.month
day = now.day
if month >= 11: return True # Nov, Dic
if month <= 3: return True # Gen, Feb, Mar
if month == 4 and day <= 15: return True # Fino al 15 Apr
return False
def load_last_state():
"""Legge se c'era un allerta attiva"""
if not os.path.exists(STATE_FILE): return False
try:
with open(STATE_FILE, 'r') as f:
data = json.load(f)
return data.get("alert_active", False)
except: return False
def save_current_state(is_active):
"""Salva lo stato corrente"""
try:
with open(STATE_FILE, 'w') as f:
json.dump({"alert_active": is_active, "updated": str(datetime.datetime.now())}, f)
except Exception as e:
print(f"Errore salvataggio stato: {e}")
def send_telegram_message(message): def send_telegram_message(message):
if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN: if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN:
print(f"[TEST OUT] {message}") print(f"[TEST OUT] {message}")
@@ -38,8 +73,7 @@ def send_telegram_message(message):
def get_forecast(lat, lon): def get_forecast(lat, lon):
url = "https://api.open-meteo.com/v1/forecast" url = "https://api.open-meteo.com/v1/forecast"
params = { params = {
"latitude": lat, "latitude": lat, "longitude": lon,
"longitude": lon,
"hourly": "snowfall", "hourly": "snowfall",
"models": "arome_france_hd", "models": "arome_france_hd",
"timezone": "Europe/Rome", "timezone": "Europe/Rome",
@@ -75,14 +109,25 @@ def calculate_sums(data):
if start_idx == -1: return None if start_idx == -1: return None
end = len(snow) end = len(snow)
s3 = sum(x for x in snow[start_idx:min(start_idx+3, end)] if x) # Calcola somme sugli orizzonti temporali
s6 = sum(x for x in snow[start_idx:min(start_idx+6, end)] if x) def get_sum(hours):
s12 = sum(x for x in snow[start_idx:min(start_idx+12, end)] if x) return sum(x for x in snow[start_idx:min(start_idx+hours, end)] if x)
s24 = sum(x for x in snow[start_idx:min(start_idx+24, end)] if x)
return {"3h": s3, "6h": s6, "12h": s12, "24h": s24} return {
"3h": get_sum(3),
"6h": get_sum(6),
"12h": get_sum(12),
"24h": get_sum(24)
}
# --- LOGICA PRINCIPALE ---
def analyze_snow(): def analyze_snow():
# 1. Controllo Stagionale
if not is_winter_season():
print("Stagione estiva. Script in pausa.")
return
now_str = datetime.datetime.now(ZoneInfo("Europe/Rome")).strftime('%H:%M') now_str = datetime.datetime.now(ZoneInfo("Europe/Rome")).strftime('%H:%M')
print(f"--- Check Meteo {now_str} ---") print(f"--- Check Meteo {now_str} ---")
@@ -90,6 +135,7 @@ def analyze_snow():
max_area_snow = 0.0 max_area_snow = 0.0
area_details = "" area_details = ""
# 2. Raccolta Dati
for p in POINTS: for p in POINTS:
data = get_forecast(p["lat"], p["lon"]) data = get_forecast(p["lat"], p["lon"])
stats = calculate_sums(data) stats = calculate_sums(data)
@@ -99,18 +145,26 @@ def analyze_snow():
if p["name"] == "🏠 Casa": if p["name"] == "🏠 Casa":
home_stats = stats home_stats = stats
# Aggiorna il massimo rilevato in zona
if stats["24h"] > max_area_snow: if stats["24h"] > max_area_snow:
max_area_snow = stats["24h"] max_area_snow = stats["24h"]
# Aggiungi ai dettagli area se c'è neve (>0) # Costruisci dettaglio se c'è neve
if stats["24h"] > 0: if stats["24h"] > 0:
area_details += f"{p['name']}: {stats['24h']:.1f}cm (12h: {stats['12h']:.1f})\n" area_details += f"{p['name']}: {stats['24h']:.1f}cm (12h: {stats['12h']:.1f})\n"
time.sleep(1) time.sleep(1)
# 3. Decisione Alert
# C'è neve se a casa o nei dintorni l'accumulo è > soglia
home_max = home_stats["24h"] if home_stats else 0.0 home_max = home_stats["24h"] if home_stats else 0.0
SNOW_DETECTED = (home_max > SOGLIA_NOTIFICA or max_area_snow > SOGLIA_NOTIFICA)
if home_max > SOGLIA_NOTIFICA or max_area_snow > SOGLIA_NOTIFICA: # Leggi stato precedente
WAS_ACTIVE = load_last_state()
# --- SCENARIO A: C'È NEVE (Nuova o Continua) ---
if SNOW_DETECTED:
def f(v): return f"**{v:.1f}**" if v > 0 else f"{v:.1f}" def f(v): return f"**{v:.1f}**" if v > 0 else f"{v:.1f}"
@@ -130,9 +184,26 @@ def analyze_snow():
msg += "🌍 Nessuna neve rilevante nei dintorni." msg += "🌍 Nessuna neve rilevante nei dintorni."
send_telegram_message(msg) send_telegram_message(msg)
save_current_state(True) # Salva che l'allerta è attiva
print("Neve rilevata. Notifica inviata.") print("Neve rilevata. Notifica inviata.")
# --- SCENARIO B: ALLARME RIENTRATO (Neve 0, ma prima c'era) ---
elif not SNOW_DETECTED and WAS_ACTIVE:
msg = (
f"🟢 **PREVISIONE NEVE ANNULLATA**\n"
f"📅 _Aggiornamento ore {now_str}_\n\n"
f"Le ultime previsioni AROME non indicano più accumuli nevosi rilevanti nelle prossime 24 ore.\n"
f"Situazione tornata alla normalità."
)
send_telegram_message(msg)
save_current_state(False) # Resetta lo stato
print("Allarme rientrato. Notifica inviata.")
# --- SCENARIO C: TUTTO TRANQUILLO (E lo era anche prima) ---
else: else:
print(f"Nessuna neve. Casa: {home_max}cm, Area Max: {max_area_snow}cm") # Aggiorna timestamp ma mantieni false
save_current_state(False)
print(f"Nessuna neve. Casa: {home_max}cm, Area: {max_area_snow}cm")
if __name__ == "__main__": if __name__ == "__main__":
analyze_snow() analyze_snow()

View File

@@ -18,7 +18,6 @@ NAS_USER = "daniely"
MASTER_IP = "192.168.128.80" MASTER_IP = "192.168.128.80"
# --- LISTE DISPOSITIVI --- # --- LISTE DISPOSITIVI ---
# Core: Dispositivi intelligenti (SSH, CPU, RAM)
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},
{"name": "🍓 Pi-2 (Backup)", "ip": "127.0.0.1", "type": "local", "user": ""}, {"name": "🍓 Pi-2 (Backup)", "ip": "127.0.0.1", "type": "local", "user": ""},
@@ -26,7 +25,6 @@ CORE_DEVICES = [
{"name": "🗄️ NAS 214", "ip": "192.168.128.90", "type": "nas", "user": NAS_USER} {"name": "🗄️ NAS 214", "ip": "192.168.128.90", "type": "nas", "user": NAS_USER}
] ]
# Infra: Solo Ping e Riavvio (Switch, WiFi)
INFRA_DEVICES = [ INFRA_DEVICES = [
{"name": "📡 Router", "ip": "192.168.128.1"}, {"name": "📡 Router", "ip": "192.168.128.1"},
{"name": "📶 WiFi Sala", "ip": "192.168.128.101"}, {"name": "📶 WiFi Sala", "ip": "192.168.128.101"},
@@ -47,27 +45,41 @@ logger = logging.getLogger(__name__)
def run_cmd(command, ip=None, user=None): def run_cmd(command, ip=None, user=None):
try: try:
if ip == "127.0.0.1" or ip is None: if ip == "127.0.0.1" or ip is None:
return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=5).decode('utf-8').strip()
else: else:
safe_cmd = command.replace("'", "'\\''") safe_cmd = command.replace("'", "'\\''")
full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=4 {user}@{ip} '{safe_cmd}'" full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} '{safe_cmd}'"
return subprocess.check_output(full_cmd, shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() return subprocess.check_output(full_cmd, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip()
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:
# Ping ultra-rapido (Timeout 1s) # Timeout aggressivo: 0.5 secondi (-W 1 è il minimo di ping standard, ma Python taglia a 0.8)
subprocess.check_call(["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 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: return "🔴" except subprocess.TimeoutExpired:
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']
uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user)
if not uptime_raw or "Err" in uptime_raw: return "🔴 **OFFLINE**" if not uptime_raw or "Err" in uptime_raw: return "🔴 **OFFLINE**"
uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0]
uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0]
temp = "N/A" temp = "N/A"
if dtype in ["pi", "local"]: if dtype in ["pi", "local"]:
t = run_cmd("cat /sys/class/thermal/thermal_zone0/temp", ip, user) t = run_cmd("cat /sys/class/thermal/thermal_zone0/temp", ip, user)
if t.isdigit(): temp = f"{int(t)/1000:.1f}°C" if t.isdigit(): temp = f"{int(t)/1000:.1f}°C"
@@ -78,6 +90,7 @@ 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}}'"
@@ -92,7 +105,7 @@ 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"
# --- GESTIONE MENU --- # --- BOT HANDLERS ---
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):
@@ -102,18 +115,16 @@ def restricted(func):
@restricted @restricted
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# MENU PRINCIPALE
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("📜 Logs", callback_data="menu_logs")]
] ]
text = "🎛 **Loogle Control Center**\nSeleziona un pannello:" text = "🎛 **Loogle Control Center v6.2**\nSeleziona un pannello:"
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: else:
# Se chiamato da un pulsante "Indietro", modifica il messaggio esistente
await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
@restricted @restricted
@@ -124,7 +135,6 @@ 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)
# --- MENU CORE ---
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}")])
@@ -143,42 +153,43 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown") await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown")
await query.edit_message_text(f"🔹 **{dev['name']}**\n\n{get_device_stats(dev)}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") 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")
# --- MENU LAN (DIAGNOSTICA) ---
elif data == "menu_lan": elif data == "menu_lan":
await query.edit_message_text("⏳ **Ping test in corso...**", parse_mode="Markdown") await query.edit_message_text("⏳ **Scansione LAN rapida...**", parse_mode="Markdown")
report = "🔍 **DIAGNOSTICA LAN**\n\n" report = "🔍 **DIAGNOSTICA LAN**\n\n"
# Genera tabella compatta try:
# Core Devices
for dev in CORE_DEVICES:
report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
report += "\n"
# Infra Devices
for dev in INFRA_DEVICES: for dev in INFRA_DEVICES:
status = get_ping_icon(dev['ip']) report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
# Aggiunge riga: Icona Nome (IP)
report += f"{status} **{dev['name']}**\n`{dev['ip']}`\n" except Exception as e:
report += f"\n⚠️ Errore imprevisto durante scansione: {e}"
keyboard = [ keyboard = [
[InlineKeyboardButton("⚡ Menu Riavvio Dispositivi", callback_data="menu_reboot")], [InlineKeyboardButton("⚡ Menu Riavvio", callback_data="menu_reboot")],
[InlineKeyboardButton("🔄 Aggiorna", callback_data="menu_lan")], [InlineKeyboardButton("🔄 Aggiorna", callback_data="menu_lan"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]
[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")
# --- MENU RIAVVIO ---
elif data == "menu_reboot": elif data == "menu_reboot":
keyboard = [] keyboard = []
for i, dev in enumerate(INFRA_DEVICES): for i, dev in enumerate(INFRA_DEVICES):
if "Router" not in dev['name']: # Protezione router if "Router" not in dev['name']: keyboard.append([InlineKeyboardButton(f"{dev['name']}", callback_data=f"reboot_{i}")])
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**\nRichiede SSH configurato sul dispositivo target (admin).", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") 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")
elif data.startswith("reboot_"): elif data.startswith("reboot_"):
dev = INFRA_DEVICES[int(data.split("_")[1])] dev = INFRA_DEVICES[int(data.split("_")[1])]
# Tenta riavvio con utente 'admin' (standard per Zyxel/AP)
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"⚡ Comando inviato a {dev['name']}...\n\nRisposta:\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown")
# --- ALTRI MENU (Pi-hole, Net, Logs) --- # --- 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 "🔴"
@@ -198,7 +209,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...**", parse_mode="Markdown") await query.edit_message_text("🚀 **Speedtest in corso...**", 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")

View File

@@ -0,0 +1,151 @@
import requests
import json
import os
import time
import datetime
from zoneinfo import ZoneInfo
# --- CONFIGURAZIONE UTENTE ---
# 👇👇 INSERISCI QUI I TUOI DATI 👇👇
TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
# --- ZONE DA MONITORARE ---
# EMR-B2 = Costa Romagnola (Rimini/Dogana)
# EMR-A2 = Alta Collina Romagnola (San Marino/Titano)
# EMR-D1 = Pianura Bolognese (Bologna Città)
TARGET_ZONES = ["EMR-B2", "EMR-A2", "EMR-D1"]
# URL Ufficiale DPC
DPC_URL = "https://raw.githubusercontent.com/pcm-dpc/DPC-Bollettini-Criticita-Idrogeologica-Idraulica/master/files/geojson/today.json"
# File di stato
STATE_FILE = "/home/daniely/docker/telegram-bot/dpc_state.json"
# Mappe
RISK_MAP = {
1: "🟡 GIALLA",
2: "🟠 ARANCIONE",
3: "🔴 ROSSA"
}
RISK_TYPES = {
"idro": "💧 Idraulico",
"idrogeo": "⛰️ Idrogeologico",
"temporali": "⚡ Temporali",
"vento": "💨 Vento",
"neve": "❄️ Neve",
"ghiaccio": "🧊 Ghiaccio",
"mare": "🌊 Mareggiate"
}
def get_zone_label(zone_id):
if zone_id == "EMR-B2": return "Rimini / Bassa RSM"
if zone_id == "EMR-A2": return "Alta RSM / Carpegna"
if zone_id == "EMR-D1": return "Bologna Città"
return zone_id
def send_telegram_message(message):
if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN:
print(f"[TEST OUT] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
for chat_id in TELEGRAM_CHAT_IDS:
try:
requests.post(url, json={"chat_id": chat_id, "text": message, "parse_mode": "Markdown"}, timeout=10)
time.sleep(0.2)
except: pass
def load_last_alert():
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, 'r') as f: return json.load(f)
except: pass
return {}
def save_current_alert(data):
try:
with open(STATE_FILE, 'w') as f: json.dump(data, f)
except: pass
def analyze_dpc():
print("--- Controllo Protezione Civile ---")
try:
r = requests.get(DPC_URL, timeout=10)
r.raise_for_status()
data = r.json()
except Exception as e:
print(f"Errore DPC: {e}")
return
today_str = datetime.datetime.now(ZoneInfo("Europe/Rome")).strftime('%d/%m/%Y')
active_alerts = {}
max_global_level = 0
# Stringa univoca per identificare se l'allerta è cambiata nel contenuto
current_alert_signature = ""
for feature in data['features']:
props = feature['properties']
zone_id = props.get('zone_id')
if zone_id in TARGET_ZONES:
zone_risks = []
for key, label in RISK_TYPES.items():
level = props.get(f"crit_{key}")
if level and level > 0:
if level > max_global_level: max_global_level = level
color_icon = "🟡" if level == 1 else "🟠" if level == 2 else "🔴"
risk_str = f"{color_icon} {label}"
zone_risks.append(risk_str)
if zone_risks:
label = get_zone_label(zone_id)
active_alerts[label] = zone_risks
# Aggiungiamo alla firma per capire se qualcosa è cambiato
current_alert_signature += f"{zone_id}:{','.join(zone_risks)}|"
# --- LOGICA DI INVIO ---
last_state = load_last_alert()
last_date = last_state.get("date")
last_sig = last_state.get("signature", "")
# Se tutto verde (livello 0)
if max_global_level == 0:
print("Nessuna allerta.")
if last_date == today_str and last_sig != "":
# Opzionale: Potremmo mandare "Allerta Rientrata", ma DPC resetta a mezzanotte.
# Per ora resettiamo solo lo stato.
save_current_alert({"date": today_str, "level": 0, "signature": ""})
return
# Invia SE:
# 1. È un giorno nuovo
# 2. OPPURE la situazione è cambiata (es. aggiunto Bologna, o passato da Giallo a Rosso)
if last_date != today_str or current_alert_signature != last_sig:
msg = f"📢 **PROTEZIONE CIVILE (Allerta)**\n"
msg += f"📅 {today_str}\n\n"
for zone_name, risks in active_alerts.items():
msg += f"📍 **{zone_name}**\n"
msg += "\n".join(risks) + "\n\n"
msg += "_Fonte: Dipartimento Protezione Civile_"
send_telegram_message(msg)
print("Allerta inviata.")
save_current_alert({"date": today_str, "level": max_global_level, "signature": current_alert_signature})
else:
print("Allerta già notificata e invariata.")
if __name__ == "__main__":
analyze_dpc()

View File

@@ -0,0 +1,134 @@
import requests
import datetime
import json
import os
import time
from dateutil import parser
from zoneinfo import ZoneInfo
# --- CONFIGURAZIONE UTENTE ---
TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
# --- COORDINATE (Strada Cà Toro, 12 - San Marino) ---
LAT = 43.9356
LON = 12.4296
LOCATION_NAME = "🏠 Casa (Strada Cà Toro)"
# Soglia Gelo (°C)
SOGLIA_GELO = 0.0
# File di stato
STATE_FILE = "/home/daniely/docker/telegram-bot/freeze_state.json"
def load_state():
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, 'r') as f: return json.load(f)
except: pass
return {"alert_active": False, "min_temp": 100.0}
def save_state(active, min_temp):
try:
with open(STATE_FILE, 'w') as f:
json.dump({"alert_active": active, "min_temp": min_temp, "updated": str(datetime.datetime.now())}, f)
except: pass
def send_telegram_message(message):
if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN:
print(f"[TEST OUT] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
for chat_id in TELEGRAM_CHAT_IDS:
try:
requests.post(url, json={"chat_id": chat_id, "text": message, "parse_mode": "Markdown"}, timeout=10)
time.sleep(0.2)
except: pass
def get_forecast():
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": LAT, "longitude": LON,
"hourly": "temperature_2m",
"timezone": "Europe/Rome",
"forecast_days": 3 # Prendiamo 3 giorni per coprire bene le 48h
}
try:
r = requests.get(url, params=params, timeout=10)
r.raise_for_status()
return r.json()
except: return None
def analyze_freeze():
print("--- Controllo Gelo ---")
data = get_forecast()
if not data: return
hourly = data.get("hourly", {})
times = hourly.get("time", [])
temps = hourly.get("temperature_2m", [])
now = datetime.datetime.now(ZoneInfo("Europe/Rome"))
limit_time = now + datetime.timedelta(hours=48)
min_temp_val = 100.0
min_temp_time = None
# Cerca la minima nelle prossime 48 ore
for i, t_str in enumerate(times):
t_obj = parser.isoparse(t_str).replace(tzinfo=ZoneInfo("Europe/Rome"))
# Filtra solo futuro prossimo (da adesso a +48h)
if t_obj > now and t_obj <= limit_time:
temp = temps[i]
if temp < min_temp_val:
min_temp_val = temp
min_temp_time = t_obj
# --- LOGICA ALLARME ---
state = load_state()
was_active = state.get("alert_active", False)
# C'è rischio gelo?
is_freezing = min_temp_val < SOGLIA_GELO
if is_freezing:
# Formatta orario
time_str = min_temp_time.strftime('%d/%m alle %H:%M')
# SCENARIO A: NUOVO GELO (o peggioramento significativo di 2 gradi)
if not was_active or min_temp_val < state.get("min_temp", 0) - 2.0:
msg = (
f"❄️ **ALLERTA GELO**\n"
f"📍 {LOCATION_NAME}\n\n"
f"Prevista temperatura minima di **{min_temp_val:.1f}°C**\n"
f"📅 Quando: {time_str}\n\n"
f"_Proteggere piante e tubature esterne._"
)
send_telegram_message(msg)
save_state(True, min_temp_val)
print(f"Allerta inviata: {min_temp_val}°C")
else:
print(f"Gelo già notificato ({min_temp_val}°C).")
# Aggiorniamo comunque la minima registrata nel file
save_state(True, min(min_temp_val, state.get("min_temp", 100)))
# SCENARIO B: ALLARME RIENTRATO
elif was_active and not is_freezing:
msg = (
f"☀️ **RISCHIO GELO RIENTRATO**\n"
f"📍 {LOCATION_NAME}\n\n"
f"Le previsioni per le prossime 48 ore indicano temperature sopra lo zero.\n"
f"Minima prevista: {min_temp_val:.1f}°C."
)
send_telegram_message(msg)
save_state(False, min_temp_val)
print("Allarme rientrato.")
else:
save_state(False, min_temp_val)
print(f"Nessun gelo. Minima: {min_temp_val}°C")
if __name__ == "__main__":
analyze_freeze()

View File

@@ -6,28 +6,38 @@ import time
from dateutil import parser from dateutil import parser
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
# --- CONFIGURAZIONE --- # --- CONFIGURAZIONE UTENTE ---
TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4" TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
TELEGRAM_CHAT_IDS = ["64463169", "132455422"] TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
# Coordinate (San Marino) # Coordinate (San Marino / Casa)
LAT = 43.9356 LAT = 43.9356
LON = 12.4296 LON = 12.4296
# SOGLIE DI ALLARME # --- SOGLIE DI ALLARME ---
WIND_LIMIT_WARN = 60.0 # km/h (Attenzione: Vasi, stendini) # Vento (Protezione Civile Emilia-Romagna)
WIND_LIMIT_CRIT = 90.0 # km/h (Pericolo) WIND_YELLOW = 62.0 # km/h
RAIN_3H_LIMIT = 25.0 # mm in 3 ore (Rischio allagamenti/disagi forti) WIND_ORANGE = 75.0 # km/h
WIND_RED = 88.0 # km/h
# File di stato per evitare spam (memorizza ultimo avviso inviato) # Pioggia
STATE_FILE = "/tmp/weather_alert_state.json" RAIN_3H_LIMIT = 25.0 # mm in 3 ore
# Codici WMO per Gelicidio (Freezing Rain)
FREEZING_CODES = [56, 57, 66, 67]
# File di stato
STATE_FILE = "/home/daniely/docker/telegram-bot/weather_state.json"
def send_telegram_message(message):
if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN:
print(f"[TEST OUT] {message}")
return
def send_telegram(msg):
if "INSERISCI" in TELEGRAM_BOT_TOKEN: return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
for cid in TELEGRAM_CHAT_IDS: for chat_id in TELEGRAM_CHAT_IDS:
try: try:
requests.post(url, json={"chat_id": cid, "text": msg, "parse_mode": "Markdown"}, timeout=5) requests.post(url, json={"chat_id": chat_id, "text": message, "parse_mode": "Markdown"}, timeout=10)
time.sleep(0.2) time.sleep(0.2)
except: pass except: pass
@@ -35,8 +45,8 @@ def get_forecast():
url = "https://api.open-meteo.com/v1/forecast" url = "https://api.open-meteo.com/v1/forecast"
params = { params = {
"latitude": LAT, "longitude": LON, "latitude": LAT, "longitude": LON,
"hourly": "precipitation,windgusts_10m", "hourly": "precipitation,windgusts_10m,weathercode",
"models": "arome_france_hd", # Alta precisione "models": "arome_france_hd",
"timezone": "Europe/Rome", "timezone": "Europe/Rome",
"forecast_days": 2 "forecast_days": 2
} }
@@ -51,12 +61,15 @@ def load_state():
try: try:
with open(STATE_FILE, 'r') as f: return json.load(f) with open(STATE_FILE, 'r') as f: return json.load(f)
except: pass except: pass
return {"last_wind": 0, "last_rain": 0, "time": 0} return {"alert_active": False, "last_wind": 0, "last_rain": 0, "wind_level": 0, "freezing_active": False}
def save_state(state): def save_state(state):
try:
with open(STATE_FILE, 'w') as f: json.dump(state, f) with open(STATE_FILE, 'w') as f: json.dump(state, f)
except: pass
def analyze(): def analyze():
print("--- Controllo Meteo Severo (Wind/Rain/Ice) ---")
data = get_forecast() data = get_forecast()
if not data: return if not data: return
@@ -64,14 +77,11 @@ def analyze():
times = hourly.get("time", []) times = hourly.get("time", [])
wind = hourly.get("windgusts_10m", []) wind = hourly.get("windgusts_10m", [])
rain = hourly.get("precipitation", []) rain = hourly.get("precipitation", [])
wcode = hourly.get("weathercode", [])
now = datetime.datetime.now(ZoneInfo("Europe/Rome")) now = datetime.datetime.now(ZoneInfo("Europe/Rome"))
state = load_state() state = load_state()
# Reset stato se sono passate più di 8 ore dall'ultimo avviso
if time.time() - state.get("time", 0) > (8 * 3600):
state = {"last_wind": 0, "last_rain": 0, "time": time.time()}
# Trova indice ora corrente # Trova indice ora corrente
start_idx = -1 start_idx = -1
for i, t in enumerate(times): for i, t in enumerate(times):
@@ -88,41 +98,93 @@ def analyze():
max_wind_time = "" max_wind_time = ""
sum_rain_3h = 0.0 sum_rain_3h = 0.0
# Cerca picco vento nelle 12h freezing_rain_detected = False
freezing_time = ""
# Cerca picchi
for i in range(start_idx, end_idx): for i in range(start_idx, end_idx):
# Vento
if wind[i] > max_wind: if wind[i] > max_wind:
max_wind = wind[i] max_wind = wind[i]
max_wind_time = parser.isoparse(times[i]).strftime('%H:%M') max_wind_time = parser.isoparse(times[i]).strftime('%H:%M')
# Cerca picco pioggia (finestra mobile 3h) # Gelicidio
if wcode[i] in FREEZING_CODES:
freezing_rain_detected = True
if not freezing_time: # Prendi il primo orario
freezing_time = parser.isoparse(times[i]).strftime('%H:%M')
# Pioggia (sliding window 3h)
for i in range(start_idx, end_idx - 3): for i in range(start_idx, end_idx - 3):
current_sum = sum(rain[i:i+3]) current_sum = sum(rain[i:i+3])
if current_sum > sum_rain_3h: if current_sum > sum_rain_3h:
sum_rain_3h = current_sum sum_rain_3h = current_sum
# --- CLASSIFICAZIONE ---
alerts = [] alerts = []
should_notify = False
# LOGICA VENTO WAS_ALARM = state.get("alert_active", False)
if max_wind > WIND_LIMIT_WARN:
# Manda avviso solo se è peggiore del precedente o se non ho mandato nulla # 1. GELO (Priorità Massima)
if max_wind > state["last_wind"] + 10: # +10km/h di tolleranza per non ripetere if freezing_rain_detected:
icon = "💨" if max_wind < WIND_LIMIT_CRIT else "🌪️ ⛔️" if not state.get("freezing_active", False):
livello = "FORTE" if max_wind < WIND_LIMIT_CRIT else "BURRASCA (Pericolo)" alerts.append(f"🧊 **ALLARME GELICIDIO**\nPrevista pioggia che gela (Freezing Rain).\n🕒 Inizio previsto: {freezing_time}\n_Pericolo ghiaccio su strada!_")
alerts.append(f"{icon} **VENTO {livello}**\nRaffiche previste fino a **{max_wind:.0f} km/h** verso le {max_wind_time}.\n_Consiglio: Ritirare oggetti leggeri/tende._") should_notify = True
state["freezing_active"] = True
else:
state["freezing_active"] = False
# 2. VENTO
wind_level_curr = 0
if max_wind > WIND_RED:
wind_level_curr = 3
msg = f"🔴 **TEMPESTA (Burrasca Fortissima)**\nRaffiche > {WIND_RED:.0f} km/h (Picco: **{max_wind:.0f}** km/h).\n🕒 Ore {max_wind_time}"
elif max_wind > WIND_ORANGE:
wind_level_curr = 2
msg = f"🟠 **VENTO MOLTO FORTE**\nRaffiche > {WIND_ORANGE:.0f} km/h (Picco: **{max_wind:.0f}** km/h).\n🕒 Ore {max_wind_time}"
elif max_wind > WIND_YELLOW:
wind_level_curr = 1
msg = f"🟡 **VENTO FORTE**\nRaffiche > {WIND_YELLOW:.0f} km/h (Picco: **{max_wind:.0f}** km/h).\n🕒 Ore {max_wind_time}"
if wind_level_curr > 0:
if not WAS_ALARM or wind_level_curr > state.get("wind_level", 0):
alerts.append(msg)
should_notify = True
state["wind_level"] = wind_level_curr
state["last_wind"] = max_wind state["last_wind"] = max_wind
state["time"] = time.time()
# LOGICA PIOGGIA # 3. PIOGGIA
if sum_rain_3h > RAIN_3H_LIMIT: if sum_rain_3h > RAIN_3H_LIMIT:
if sum_rain_3h > state["last_rain"] + 10: if not WAS_ALARM or sum_rain_3h > state.get("last_rain", 0) + 10:
alerts.append(f"🌧️ **PIOGGIA INTENSA**\nPrevisti **{sum_rain_3h:.1f} mm** in sole 3 ore.\n_Possibili disagi stradali._") alerts.append(f"🌧️ **PIOGGIA INTENSA**\nPrevisti **{sum_rain_3h:.1f} mm** in 3 ore.")
state["last_rain"] = sum_rain_3h state["last_rain"] = sum_rain_3h
state["time"] = time.time() should_notify = True
if alerts: # --- INVIO ---
full_msg = f"⚠️ **AVVISO METEO (Prossime 12h)**\n\n" + "\n\n".join(alerts) IS_ALARM_NOW = (wind_level_curr > 0) or (sum_rain_3h > RAIN_3H_LIMIT) or freezing_rain_detected
if should_notify and alerts:
full_msg = f"⚠️ **AVVISO METEO SEVERO**\n\n" + "\n\n".join(alerts)
send_telegram_message(full_msg) send_telegram_message(full_msg)
state["alert_active"] = True
save_state(state) save_state(state)
print("Allerta inviata.")
elif WAS_ALARM and not IS_ALARM_NOW:
# Allarme rientrato
msg = f"🟢 **ALLERTA METEO RIENTRATA**\nLe condizioni sono tornate sotto le soglie di guardia."
send_telegram_message(msg)
state = {"alert_active": False, "last_wind": 0, "last_rain": 0, "wind_level": 0, "freezing_active": False}
save_state(state)
print("Allarme rientrato.")
else:
# Aggiorna stato parziale se serve (es. vento calato ma ancora presente)
if not IS_ALARM_NOW:
state["alert_active"] = False
save_state(state)
print(f"Nessun nuovo allarme. W:{max_wind:.0f} R:{sum_rain_3h:.1f} Ice:{freezing_rain_detected}")
if __name__ == "__main__": if __name__ == "__main__":
analyze() analyze()

View File

@@ -0,0 +1,191 @@
import requests
import datetime
import time
import json
import os
from dateutil import parser
from zoneinfo import ZoneInfo
# --- CONFIGURAZIONE UTENTE ---
# 👇👇 INSERISCI QUI I TUOI DATI 👇👇
TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
# --- SOGLIE DI ALLARME (Bologna) ---
SOGLIA_NEVE = 0.0 # cm (Basta neve per attivare)
SOGLIA_PIOGGIA_3H = 30.0 # mm in 3 ore (Pioggia molto forte)
# File di stato per la memoria
STATE_FILE = "/home/daniely/docker/telegram-bot/student_state.json"
# --- PUNTI DEL PERCORSO (Caselli A14) ---
POINTS = [
{"name": "🎓 Bologna (V. Regnoli)", "lat": 44.4930, "lon": 11.3690, "type": "trigger"},
{"name": "🛣️ Casello Imola", "lat": 44.3798, "lon": 11.7397, "type": "route"},
{"name": "🛣️ Casello Faenza", "lat": 44.3223, "lon": 11.9040, "type": "route"},
{"name": "🛣️ Casello Forlì", "lat": 44.2502, "lon": 12.0910, "type": "route"},
{"name": "🛣️ Casello Cesena", "lat": 44.1675, "lon": 12.2835, "type": "route"},
{"name": "🛣️ Casello Rimini", "lat": 44.0362, "lon": 12.5659, "type": "route"},
{"name": "🏠 San Marino", "lat": 43.9356, "lon": 12.4296, "type": "end"}
]
# --- FUNZIONI DI UTILITÀ ---
def load_last_state():
"""Legge se c'era un allerta attiva"""
if not os.path.exists(STATE_FILE): return False
try:
with open(STATE_FILE, 'r') as f:
data = json.load(f)
return data.get("alert_active", False)
except: return False
def save_current_state(is_active):
"""Salva lo stato corrente"""
try:
with open(STATE_FILE, 'w') as f:
json.dump({"alert_active": is_active, "updated": str(datetime.datetime.now())}, f)
except Exception as e:
print(f"Errore salvataggio stato: {e}")
def send_telegram_message(message):
if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN:
print(f"[TEST OUT] {message}")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
for chat_id in TELEGRAM_CHAT_IDS:
try:
requests.post(url, json={"chat_id": chat_id, "text": message, "parse_mode": "Markdown"}, timeout=10)
time.sleep(0.2)
except: pass
def get_forecast(lat, lon):
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": lat, "longitude": lon,
"hourly": "precipitation,snowfall",
"models": "arome_france_hd",
"timezone": "Europe/Rome",
"forecast_days": 2
}
try:
res = requests.get(url, params=params, timeout=5)
res.raise_for_status()
return res.json()
except: return None
def get_stats(data):
if not data: return None
hourly = data.get("hourly", {})
times = hourly.get("time", [])
snow = hourly.get("snowfall", [])
rain = hourly.get("precipitation", [])
now = datetime.datetime.now(ZoneInfo("Europe/Rome"))
start_idx = -1
for i, t in enumerate(times):
if parser.isoparse(t).replace(tzinfo=ZoneInfo("Europe/Rome")) >= now.replace(minute=0,second=0,microsecond=0):
start_idx = i
break
if start_idx == -1: return None
limit = min(start_idx + 24, len(times))
def sum_slice(arr, hours):
return sum(x for x in arr[start_idx:min(start_idx+hours, limit)] if x)
return {
"snow_3h": sum_slice(snow, 3),
"snow_6h": sum_slice(snow, 6),
"snow_12h": sum_slice(snow, 12),
"snow_24h": sum_slice(snow, 24),
"rain_3h": sum_slice(rain, 3),
"rain_max": max(rain[start_idx:limit]) if rain else 0
}
# --- LOGICA PRINCIPALE ---
def main():
print("--- Analisi Studente Bologna ---")
now_str = datetime.datetime.now(ZoneInfo("Europe/Rome")).strftime('%H:%M')
# 1. ANALISI BOLOGNA (Il Trigger)
bo_point = POINTS[0]
bo_data = get_forecast(bo_point["lat"], bo_point["lon"])
bo_stats = get_stats(bo_data)
if not bo_stats: return
# Controlla se scatta l'allarme
alarm_snow = bo_stats["snow_24h"] > SOGLIA_NEVE
alarm_rain = bo_stats["rain_3h"] > SOGLIA_PIOGGIA_3H
# Carica stato precedente
WAS_ACTIVE = load_last_state()
# --- SCENARIO A: C'È ALLERTA ---
if alarm_snow or alarm_rain:
icon_main = "❄️" if alarm_snow else "🌧️"
msg = f"{icon_main} **ALLERTA METEO BOLOGNA**\n"
msg += f"📅 _Aggiornamento ore {now_str}_\n\n"
# Dettaglio Bologna
msg += f"🎓 **A BOLOGNA:**\n"
if alarm_snow:
msg += f"• Neve 3h: **{bo_stats['snow_3h']:.1f}** cm\n"
msg += f"• Neve 6h: **{bo_stats['snow_6h']:.1f}** cm\n"
msg += f"• Neve 12h: **{bo_stats['snow_12h']:.1f}** cm\n"
msg += f"• Neve 24h: **{bo_stats['snow_24h']:.1f}** cm\n"
if alarm_rain:
msg += f"• Pioggia 3h: **{bo_stats['rain_3h']:.1f}** mm (Intensa!)\n"
msg += "\n🚗 **SITUAZIONE AI CASELLI (A14):**\n"
# 2. ANALISI PERCORSO (Solo se c'è allerta)
route_issues = False
for p in POINTS[1:]:
stats = get_stats(get_forecast(p["lat"], p["lon"]))
if not stats: continue
has_snow = stats["snow_24h"] > 0
has_rain = stats["rain_3h"] > 5.0
if has_snow or has_rain:
route_issues = True
line = f"**{p['name']}**: "
if has_snow: line += f"❄️ {stats['snow_12h']:.1f}cm (12h) "
if has_rain: line += f"🌧️ {stats['rain_3h']:.1f}mm "
msg += f"{line}\n"
if not route_issues:
msg += "✅ I caselli autostradali sembrano puliti."
send_telegram_message(msg)
save_current_state(True)
print("Allerta inviata.")
# --- SCENARIO B: ALLARME RIENTRATO ---
elif not (alarm_snow or alarm_rain) and WAS_ACTIVE:
msg = (
f"🟢 **ALLARME RIENTRATO (Bologna)**\n"
f"📅 _Aggiornamento ore {now_str}_\n\n"
f"Le previsioni non indicano più neve o piogge critiche per le prossime 24 ore.\n"
f"Situazione tornata alla normalità."
)
send_telegram_message(msg)
save_current_state(False)
print("Allarme rientrato. Notifica inviata.")
# --- SCENARIO C: TRANQUILLO ---
else:
save_current_state(False)
print("Nessuna allerta.")
if __name__ == "__main__":
main()