#!/usr/bin/env python3 import requests import argparse import datetime import os import sys from zoneinfo import ZoneInfo from collections import defaultdict # --- CONFIGURAZIONE DEFAULT --- DEFAULT_LAT = 43.9356 DEFAULT_LON = 12.4296 DEFAULT_NAME = "🏠 Casa (Strada Cà Toro)" # --- TIMEZONE --- TZ_STR = "Europe/Rome" # --- TELEGRAM CONFIG --- ADMIN_CHAT_ID = "64463169" TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token") TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token" # --- SOGLIE --- SOGLIA_VENTO_KMH = 40.0 MIN_MM_PER_EVENTO = 0.1 def get_bot_token(): for path in [TOKEN_FILE_ETC, TOKEN_FILE_HOME]: if os.path.exists(path): try: with open(path, 'r') as f: return f.read().strip() except: pass sys.exit(1) def get_coordinates(query): if not query or query.lower() == "casa": return DEFAULT_LAT, DEFAULT_LON, DEFAULT_NAME url = "https://geocoding-api.open-meteo.com/v1/search" try: resp = requests.get(url, params={"name": query, "count": 1, "language": "it", "format": "json"}, timeout=5) res = resp.json().get("results", [])[0] return res['latitude'], res['longitude'], f"{res.get('name')} ({res.get('country_code','')})" except: return None, None, None def get_weather(lat, lon): url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "hourly": "temperature_2m,precipitation_probability,precipitation,weathercode,windspeed_10m,dewpoint_2m", "daily": "temperature_2m_max,temperature_2m_min,sunrise,sunset", "timezone": TZ_STR, "models": "best_match", "forecast_days": 8 } try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except: return None def get_precip_type(code): """Definisce il tipo di precipitazione in base al codice WMO.""" # Neve (71-77, 85-86) if (71 <= code <= 77) or code in [85, 86]: return "❄️ Neve" # Grandine (96-99) if code in [96, 99]: return "⚡🌨 Grandine" # Pioggia congelantesi (66-67) if code in [66, 67]: return "🧊☔ Pioggia Congelantesi" # Pioggia standard return "☔ Pioggia" def get_intensity_label(mm_h): if mm_h < 2.5: return "Debole" if mm_h < 7.6: return "Moderata" return "Forte ⚠️" def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints): """Scansiona le 24 ore e trova blocchi di eventi continui.""" events = [] # ========================================== # 1. LIVELLO PERICOLI (Ghiaccio, Gelo, Brina) # ========================================== in_ice = False start_ice = 0 ice_type = "" for i in range(len(times)): t = temps[i] d = dewpoints[i] p = precip[i] c = codes[i] current_ice_condition = None # A. GELICIDIO (Pericolo massimo) # Se il codice è esplicitamente Gelicidio (66,67) OPPURE piove (codici pioggia) con T < 0 is_raining_code = (50 <= c <= 69) or (80 <= c <= 82) if c in [66, 67] or (p > 0 and t <= 0 and is_raining_code): current_ice_condition = "🧊☠️ GELICIDIO" # B. GHIACCIO/BRINA (Strada Scivolosa) # Niente precipitazioni, T bassa (<2°C) e DewPoint vicinissimo alla T (<1°C diff) elif p == 0 and t <= 2.0 and (t - d) < 1.0: current_ice_condition = "⛸️⚠️ GHIACCIO/BRINA (Strada Scivolosa)" # C. GELATA SEMPLICE (T < 0) elif t < 0: current_ice_condition = "❄️ Gelata notturna" # Logica raggruppamento if current_ice_condition and not in_ice: in_ice = True start_ice = i ice_type = current_ice_condition elif (not current_ice_condition and in_ice) or (in_ice and current_ice_condition != ice_type) or (in_ice and i == len(times)-1): end_idx = i if not current_ice_condition else i if end_idx > start_ice: start_time = times[start_ice].split("T")[1][:5] end_time = times[end_idx].split("T")[1][:5] min_t_block = min(temps[start_ice:end_idx+1]) events.append(f"{ice_type}: {start_time}-{end_time} (Min: {min_t_block}°C)") in_ice = False if current_ice_condition: in_ice = True start_ice = i ice_type = current_ice_condition # ========================================== # 2. LIVELLO PRECIPITAZIONI (Pioggia, Neve) # ========================================== # Nota: Non sopprimiamo più nulla. Se nevica mentre gela, li segnaliamo entrambi. in_rain = False start_idx = 0 current_rain_type = "" for i in range(len(times)): is_raining = precip[i] >= MIN_MM_PER_EVENTO if is_raining and not in_rain: in_rain = True start_idx = i current_rain_type = get_precip_type(codes[i]) # Cambio tipo precipitazione (es. da Pioggia a Neve nello stesso blocco) elif in_rain and is_raining and get_precip_type(codes[i]) != current_rain_type: # Chiudiamo il blocco precedente e ne apriamo uno nuovo end_idx = i block_precip = precip[start_idx:end_idx] tot_mm = sum(block_precip) max_prob = max(probs[start_idx:end_idx]) start_time = times[start_idx].split("T")[1][:5] end_time = times[end_idx].split("T")[1][:5] # Qui combacia avg_intensity = tot_mm / len(block_precip) events.append( f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n" f" 🕒 {start_time}-{end_time} | 💧 {tot_mm:.1f}mm | Prob: {max_prob}%" ) # Riavvia nuovo tipo start_idx = i current_rain_type = get_precip_type(codes[i]) elif (not is_raining and in_rain) or (in_rain and i == len(times)-1): in_rain = False end_idx = i if not is_raining else i + 1 block_precip = precip[start_idx:end_idx] tot_mm = sum(block_precip) if tot_mm > 0: max_prob = max(probs[start_idx:end_idx]) start_time = times[start_idx].split("T")[1][:5] end_time = times[end_idx-1].split("T")[1][:5] avg_intensity = tot_mm / len(block_precip) events.append( f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n" f" 🕒 {start_time}-{end_time} | 💧 {tot_mm:.1f}mm | Prob: {max_prob}%" ) # ========================================== # 3. LIVELLO VENTO # ========================================== max_wind = max(winds) if max_wind > SOGLIA_VENTO_KMH: peak_idx = winds.index(max_wind) peak_time = times[peak_idx].split("T")[1][:5] events.append(f"💨 Vento Forte: Picco {max_wind}km/h alle {peak_time}") return events def format_report(data, location_name): hourly = data['hourly'] daily = data['daily'] msg = f"🌍 METEO ALERT: {location_name.upper()}\n" msg += f"📡 Modelli: AROME/ICON HD\n\n" daily_map = defaultdict(list) for i, t in enumerate(hourly['time']): daily_map[t.split("T")[0]].append(i) count = 0 for day_date, indices in daily_map.items(): if count >= 7: break d_times = [hourly['time'][i] for i in indices] d_codes = [hourly['weathercode'][i] for i in indices] d_probs = [hourly['precipitation_probability'][i] for i in indices] d_precip = [hourly['precipitation'][i] for i in indices] d_winds = [hourly['windspeed_10m'][i] for i in indices] d_temps = [hourly['temperature_2m'][i] for i in indices] d_dews = [hourly['dewpoint_2m'][i] for i in indices] try: t_min = daily['temperature_2m_min'][count] t_max = daily['temperature_2m_max'][count] except: t_min, t_max = min(d_temps), max(d_temps) events_list = analyze_daily_events(d_times, d_codes, d_probs, d_precip, d_winds, d_temps, d_dews) dt = datetime.datetime.strptime(day_date, "%Y-%m-%d") day_str = dt.strftime("%a %d/%m") msg += f"📅 {day_str} 🌡️ {t_min:.0f}°/{t_max:.0f}°C\n" if events_list: for ev in events_list: msg += f" ➤ {ev}\n" else: msg += " ✅ Nessun fenomeno rilevante\n" msg += "\n" count += 1 return msg def send_telegram(text, chat_id, token): requests.post(f"https://api.telegram.org/bot{token}/sendMessage", json={"chat_id": chat_id, "text": text, "parse_mode": "HTML"}) def main(): parser = argparse.ArgumentParser() parser.add_argument("query", nargs="?", default="casa") parser.add_argument("--chat_id") parser.add_argument("--debug", action="store_true") args = parser.parse_args() token = get_bot_token() dest_chat = args.chat_id if args.chat_id and not args.debug else ADMIN_CHAT_ID lat, lon, name = get_coordinates(args.query) if not lat: return send_telegram(f"❌ '{args.query}' non trovato.", dest_chat, token) data = get_weather(lat, lon) if not data: return send_telegram("❌ Errore dati meteo.", dest_chat, token) send_telegram(format_report(data, name), dest_chat, token) if __name__ == "__main__": main()