Backup automatico script del 2026-01-11 07:00

This commit is contained in:
2026-01-11 07:00:03 +01:00
parent 2859b95dbc
commit 4555d6615e
20 changed files with 13373 additions and 887 deletions

View File

@@ -4,8 +4,11 @@ import datetime
import argparse
import sys
import logging
import os
import time
from typing import Optional, List
from zoneinfo import ZoneInfo
from dateutil import parser as date_parser # pyright: ignore[reportMissingModuleSource]
from dateutil import parser as date_parser
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -15,16 +18,77 @@ logger = logging.getLogger(__name__)
HOME_LAT = 43.9356
HOME_LON = 12.4296
HOME_NAME = "🏠 Casa (Wide View ±12km)"
TZ = "Europe/Rome"
TZ = "Europe/Berlin"
TZINFO = ZoneInfo(TZ)
# Offset ~12-15km
# Offset ~12-15km per i 5 punti
OFFSET_LAT = 0.12
OFFSET_LON = 0.16
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
HTTP_HEADERS = {"User-Agent": "loogle-bot-v10.4"}
HTTP_HEADERS = {"User-Agent": "loogle-bot-v10.5"}
# --- TELEGRAM CONFIG ---
TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token")
TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token"
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:
return ""
except Exception as e:
logger.error(f"Error reading {path}: {e}")
return ""
def load_bot_token() -> str:
tok = os.environ.get("TELEGRAM_BOT_TOKEN", "").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 telegram_send_markdown(message_md: str, chat_ids: Optional[List[str]] = None) -> bool:
"""Invia messaggio Markdown a Telegram. Returns True se almeno un invio è riuscito."""
token = load_bot_token()
if not token:
logger.warning("Telegram token missing: message not sent.")
return False
if chat_ids is None:
return False # Se non specificato, non inviare
url = f"https://api.telegram.org/bot{token}/sendMessage"
base_payload = {
"text": message_md,
"parse_mode": "Markdown",
"disable_web_page_preview": True,
}
sent_ok = False
with requests.Session() as s:
for chat_id in chat_ids:
payload = dict(base_payload)
payload["chat_id"] = chat_id
try:
resp = s.post(url, json=payload, timeout=15)
if resp.status_code == 200:
sent_ok = True
else:
logger.error("Telegram error chat_id=%s status=%s body=%s",
chat_id, resp.status_code, resp.text[:500])
time.sleep(0.25)
except Exception as e:
logger.exception("Telegram exception chat_id=%s err=%s", chat_id, e)
return sent_ok
def now_local() -> datetime.datetime:
return datetime.datetime.now(TZINFO)
@@ -44,15 +108,30 @@ def degrees_to_cardinal(d: int) -> str:
return dirs[round(d / 45) % 8]
except: return "N"
# --- HELPER SICUREZZA DATI ---
def get_val(val, default=0.0):
if val is None: return default
return float(val)
def safe_get_list(hourly_data, key, length, default=None):
if key in hourly_data and hourly_data[key] is not None:
return hourly_data[key]
return [default] * length
def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape, cloud_type):
sky = "☁️"
try:
# LOGICA NEVE (v10.5 Fix):
# È neve se c'è accumulo OPPURE se il codice meteo dice neve (anche senza accumulo)
is_snowing = snow > 0 or (code in [71, 73, 75, 77, 85, 86])
if cloud_type == 'F':
sky = "🌫️"
elif code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️"
elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️"
elif prec >= 0.1:
sky = "🌨️" if is_snowing else "🌧️"
else:
# LOGICA PERCEZIONE UMANA
# LOGICA PERCEZIONE UMANA (Nubi Alte vs Basse)
if cloud_type == 'H':
if cloud <= 40: sky = "☀️" if is_day else "🌙"
elif cloud <= 80: sky = "🌤️" if is_day else "🌙"
@@ -65,7 +144,8 @@ def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape, c
else: sky = "☁️"
sgx = "-"
if snow > 0 or (code is not None and code in (71,73,75,77,85,86)): sgx = "☃️"
# Simbolo laterale (Priorità agli eventi pericolosi)
if is_snowing: sgx = "☃️"
elif temp < 0 or (code is not None and code in (66,67)): sgx = "🧊"
elif cape > 2000: sgx = "🌪️"
elif cape > 1000: sgx = ""
@@ -92,15 +172,36 @@ def get_coordinates(city_name: str):
logger.error(f"Geocoding error: {e}")
return None
def choose_best_model(lat, lon, cc):
if cc == 'JP': return "jma_msm", "JMA MSM"
if cc in ['NO', 'SE', 'FI', 'DK', 'IS']: return "metno_nordic", "Yr.no"
if cc in ['GB', 'IE']: return "ukmo_global", "UK MetOffice"
if cc == 'IT' or cc == 'SM': return "meteofrance_arome_france_hd", "AROME HD"
if cc in ['DE', 'AT', 'CH', 'LI', 'FR']: return "icon_d2", "ICON-D2"
return "gfs_global", "NOAA GFS"
def choose_best_model(lat, lon, cc, is_home=False):
"""
Sceglie il modello meteo.
- Per Casa: usa AROME Seamless (ha snowfall)
- Per altre località: usa best match di Open-Meteo (senza specificare models)
"""
if is_home:
# Per Casa, usa AROME Seamless (ha snowfall e dati dettagliati)
return "meteofrance_seamless", "AROME HD"
else:
# Per query worldwide, usa best match (Open-Meteo sceglie automaticamente)
return None, "Best Match"
def get_forecast(lat, lon, model):
def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after_60s=False):
"""
Recupera forecast. Se model è None, usa best match di Open-Meteo.
Per Casa (is_home=True), usa AROME Seamless.
Args:
retry_after_60s: Se True, attende 10 secondi prima di riprovare (per retry)
"""
# Usa timezone personalizzata se fornita, altrimenti default
tz_to_use = timezone if timezone else TZ
# Se è un retry, attendi 10 secondi (ridotto da 60s per evitare timeout esterni)
if retry_after_60s:
logger.info("Attendo 10 secondi prima del retry...")
time.sleep(10)
# Generiamo 5 punti: Centro, N, S, E, W
lats = [lat, lat + OFFSET_LAT, lat - OFFSET_LAT, lat, lat]
lons = [lon, lon, lon, lon + OFFSET_LON, lon - OFFSET_LON]
@@ -108,38 +209,132 @@ def get_forecast(lat, lon, model):
lon_str = ",".join(map(str, lons))
params = {
"latitude": lat_str, "longitude": lon_str, "timezone": TZ,
"latitude": lat_str, "longitude": lon_str, "timezone": tz_to_use,
"forecast_days": 3,
"models": model,
"wind_speed_unit": "kmh", "precipitation_unit": "mm",
"hourly": "temperature_2m,apparent_temperature,relative_humidity_2m,cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility,uv_index"
}
# Aggiungi models solo se specificato (per Casa usa AROME, per altre località best match)
if model:
params["models"] = model
# Nota: minutely_15 non è usato in meteo.py (solo per script di allerta)
try:
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
if r.status_code != 200:
logger.error(f"API Error {r.status_code}: {r.text}")
return None
return r.json()
# Dettagli errore più specifici
error_details = f"Status {r.status_code}"
try:
error_json = r.json()
if "reason" in error_json:
error_details += f": {error_json['reason']}"
elif "error" in error_json:
error_details += f": {error_json['error']}"
else:
error_details += f": {r.text[:200]}"
except:
error_details += f": {r.text[:200]}"
logger.error(f"API Error {error_details}")
return None, error_details # Restituisce anche i dettagli dell'errore
response_data = r.json()
# Open-Meteo per multiple locations (lat/lon separati da virgola) restituisce
# direttamente un dict con "hourly", "daily", etc. che contiene liste di valori
# per ogni location. Per semplicità, restituiamo il dict così com'è
# e lo gestiamo nel codice chiamante
return response_data, None
except requests.exceptions.Timeout as e:
error_details = f"Timeout dopo 25s: {str(e)}"
logger.error(f"Request timeout: {error_details}")
return None, error_details
except requests.exceptions.ConnectionError as e:
error_details = f"Errore connessione: {str(e)}"
logger.error(f"Connection error: {error_details}")
return None, error_details
except Exception as e:
logger.error(f"Request error: {e}")
return None
error_details = f"Errore richiesta: {type(e).__name__}: {str(e)}"
logger.error(f"Request error: {error_details}")
return None, error_details
def safe_get_list(hourly_data, key, length, default=None):
if key in hourly_data and hourly_data[key] is not None:
return hourly_data[key]
return [default] * length
def get_val(val, default=0.0):
if val is None: return default
return float(val)
def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT") -> str:
model_id, model_name = choose_best_model(lat, lon, cc)
def get_visibility_forecast(lat, lon):
"""
Recupera visibilità per località dove il modello principale non la fornisce.
Prova prima ECMWF IFS, poi fallback a best match (GFS o ICON-D2).
"""
# Prova prima con ECMWF IFS
params_ecmwf = {
"latitude": lat,
"longitude": lon,
"timezone": TZ,
"forecast_days": 3,
"models": "ecmwf_ifs04",
"hourly": "visibility"
}
try:
r = requests.get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=15)
if r.status_code == 200:
data = r.json()
hourly = data.get("hourly", {})
vis = hourly.get("visibility", [])
# Verifica se ci sono valori validi (non tutti None)
if vis and any(v is not None for v in vis):
return vis
except Exception as e:
logger.debug(f"ECMWF IFS visibility request error: {e}")
# Fallback: usa best match (senza models) che seleziona automaticamente GFS o ICON-D2
params_best = {
"latitude": lat,
"longitude": lon,
"timezone": TZ,
"forecast_days": 3,
"hourly": "visibility"
}
try:
r = requests.get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=15)
if r.status_code == 200:
data = r.json()
hourly = data.get("hourly", {})
return hourly.get("visibility", [])
except Exception as e:
logger.error(f"Visibility request error: {e}")
return None
def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT", timezone=None) -> str:
# Determina se è Casa
is_home = (abs(lat - HOME_LAT) < 0.01 and abs(lon - HOME_LON) < 0.01)
# Usa timezone personalizzata se fornita, altrimenti default
tz_to_use = timezone if timezone else TZ
model_id, model_name = choose_best_model(lat, lon, cc, is_home=is_home)
# Tentativo 1: Richiesta iniziale
data_list, error_details = get_forecast(lat, lon, model_id, is_home=is_home, timezone=tz_to_use, retry_after_60s=False)
# Se fallisce e siamo a Casa con AROME, prova retry dopo 10 secondi
if not data_list and is_home and model_id == "meteofrance_seamless":
logger.warning(f"Primo tentativo AROME fallito: {error_details}. Retry dopo 10 secondi...")
data_list, error_details = get_forecast(lat, lon, model_id, is_home=is_home, timezone=tz_to_use, retry_after_60s=True)
# Se ancora fallisce e siamo a Casa, fallback a best match
if not data_list and is_home:
logger.warning(f"AROME fallito anche dopo retry: {error_details}. Fallback a best match...")
data_list, error_details = get_forecast(lat, lon, None, is_home=False, timezone=tz_to_use, retry_after_60s=False)
if data_list:
model_name = "Best Match (fallback)"
logger.info("Fallback a best match riuscito")
# Se ancora fallisce, restituisci errore dettagliato
if not data_list:
error_msg = f"❌ Errore API Meteo ({model_name})"
if error_details:
error_msg += f"\n\nDettagli: {error_details}"
return error_msg
data_list = get_forecast(lat, lon, model_id)
if not data_list: return f"❌ Errore API Meteo ({model_name})."
if not isinstance(data_list, list): data_list = [data_list]
# Punto centrale (Casa) per dati specifici
data_center = data_list[0]
hourly_c = data_center.get("hourly", {})
times = hourly_c.get("time", [])
@@ -163,16 +358,24 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT")
l_vis = safe_get_list(hourly_c, "visibility", L, 10000)
l_uv = safe_get_list(hourly_c, "uv_index", L, 0)
# Estraggo anche i dati nuvole LOCALI per il tipo
# Se è Casa e AROME non fornisce visibilità (tutti None), recuperala da best match
if is_home and model_id == "meteofrance_seamless":
vis_check = [v for v in l_vis if v is not None]
if not vis_check: # Tutti None, recupera da best match
vis_data = get_visibility_forecast(lat, lon)
if vis_data and len(vis_data) >= L:
l_vis = vis_data[:L]
# Dati nuvole LOCALI per decidere il TIPO (L, M, H, F)
l_cl_tot_loc = safe_get_list(hourly_c, "cloud_cover", L, 0) # Copertura totale locale
l_cl_low_loc = safe_get_list(hourly_c, "cloud_cover_low", L, 0)
l_cl_mid_loc = safe_get_list(hourly_c, "cloud_cover_mid", L, 0)
l_cl_hig_loc = safe_get_list(hourly_c, "cloud_cover_high", L, 0)
# --- DATI GLOBALI (MEDIA) ---
# --- DATI GLOBALI (MEDIA 5 PUNTI) ---
acc_cl_tot = [0.0] * L
points_cl_tot = [ [] for _ in range(L) ]
p_names = ["Casa", "Nord", "Sud", "Est", "Ovest"]
for d in data_list:
h = d.get("hourly", {})
for i in range(L):
@@ -181,6 +384,7 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT")
cm = get_val(safe_get_list(h, "cloud_cover_mid", L)[i])
ch = get_val(safe_get_list(h, "cloud_cover_high", L)[i])
# Calcolo robusto del totale per singolo punto
real_point_total = max(cc, cl, cm, ch)
acc_cl_tot[i] += real_point_total
@@ -189,8 +393,9 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT")
num_points = len(data_list)
avg_cl_tot = [x / num_points for x in acc_cl_tot]
# --- DEBUG MODE ---
if debug_mode:
output = f"🔍 **DEBUG 5 PUNTI (V10.4)**\n"
output = f"🔍 **DEBUG METEO (v10.5)**\n"
now_h = now_local().replace(minute=0, second=0, microsecond=0)
idx = 0
for i, t_str in enumerate(times):
@@ -201,134 +406,195 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT")
# Valori Locali
loc_L = get_val(l_cl_low_loc[idx])
loc_H = get_val(l_cl_hig_loc[idx])
code_now = int(get_val(l_code[idx]))
output += f"Ora: {parse_time(times[idx]).strftime('%H:%M')}\n"
output += f"📍 **LOCALE (Casa)**: L:{int(loc_L)}% | M:{int(get_val(l_cl_mid_loc[idx]))}% | H:{int(loc_H)}%\n"
output += f"📍 **LOCALE (Casa)**: L:{int(loc_L)}% | H:{int(loc_H)}%\n"
output += f"🌍 **MEDIA GLOBALE**: {int(avg_cl_tot[idx])}%\n"
output += f"❄️ **NEVE**: Codice={code_now}, Accumulo={get_val(l_snow[idx])}cm\n"
decision = "H"
if loc_L > 40: decision = "L (Priorità Locale)"
output += f"👉 **Decisione**: {decision}\n"
output += f"👉 **Decisione Nuvole**: {decision}\n"
return output
now = now_local().replace(minute=0, second=0, microsecond=0)
blocks = []
header = f"{'LT':<2} {'':>4} {'h%':>3} {'mm':>3} {'Vento':<5} {'Nv%':>5} {'Sk':<2} {'Sx':<2}"
separator = "-" * 31
# --- GENERAZIONE TABELLA ---
# Usa timezone personalizzata se fornita
tz_to_use_info = ZoneInfo(tz_to_use) if tz_to_use else TZINFO
now_local_tz = datetime.datetime.now(tz_to_use_info)
for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]:
end_time = now + datetime.timedelta(hours=hours_duration)
lines = [header, separator]
count = 0
# Inizia dall'ora corrente (arrotondata all'ora)
current_hour = now_local_tz.replace(minute=0, second=0, microsecond=0)
# Fine finestra: 48 ore dopo current_hour
end_hour = current_hour + datetime.timedelta(hours=48)
# Raccogli tutti i timestamp validi nelle 48 ore successive
valid_indices = []
for i, t_str in enumerate(times):
try:
dt = parse_time(t_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=tz_to_use_info)
else:
dt = dt.astimezone(tz_to_use_info)
# Include solo timestamp >= current_hour e < end_hour
if current_hour <= dt < end_hour:
valid_indices.append((i, dt))
except Exception as e:
logger.error(f"Errore parsing timestamp {i}: {e}")
continue
if not valid_indices:
return f"❌ Nessun dato disponibile per le prossime 48 ore (da {current_hour.strftime('%H:%M')})."
# Separa in blocchi per giorno: cambia intestazione quando passa da 23 a 00
blocks = []
header = f"{'LT':<2} {'':>4} {'h%':>3} {'mm':<3} {'Vento':<5} {'Nv%':>5} {'Sk':<2} {'Sx':<2}"
separator = "-" * 31
current_day = None
current_block_lines = []
hours_from_start = 0 # Contatore ore dall'inizio (0-47)
for idx, dt in valid_indices:
# Determina se questo timestamp appartiene a un nuovo giorno
# (passaggio da 23 a 00)
day_date = dt.date()
is_new_day = (current_day is not None and day_date != current_day)
for i, t_str in enumerate(times):
try:
dt = parse_time(t_str)
if dt < now or dt >= end_time: continue
if dt.hour % step != 0: continue
T = get_val(l_temp[i], 0)
App = get_val(l_app[i], 0)
Rh = int(get_val(l_rh[i], 50))
t_suffix = ""
diff = App - T
if diff <= -2.5: t_suffix = "W"
elif diff >= 2.5: t_suffix = "H"
t_s = f"{int(round(T))}{t_suffix}"
# Determina se mostrare questo timestamp in base alla posizione nelle 48h
# Prime 24h: ogni ora (step=1)
# Dalla 25a alla 48a: ogni 2 ore (step=2)
if hours_from_start < 24:
step = 1 # Prime 24h: dettaglio 1 ora
else:
step = 2 # Dalla 25a alla 48a: dettaglio 2 ore
# Controlla se questo timestamp deve essere mostrato
should_show = (hours_from_start % step == 0)
# Se è un nuovo giorno, chiudi il blocco precedente
if is_new_day and current_block_lines:
# Chiudi blocco precedente (solo se ha contenuto oltre header e separator)
if len(current_block_lines) > 2:
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][current_day.weekday()]} {current_day.day}"
blocks.append(f"*{day_label}*\n```text\n" + "\n".join(current_block_lines) + "\n```")
current_block_lines = []
# Aggiorna current_day se è cambiato
if current_day is None or is_new_day:
current_day = day_date
# Mostra questo timestamp solo se deve essere incluso
if should_show:
# Se è il primo elemento di questo blocco (o primo elemento dopo cambio giorno), aggiungi header
if not current_block_lines:
# Assicurati che current_day corrisponda al giorno della prima riga mostrata
current_day = day_date
current_block_lines.append(header)
current_block_lines.append(separator)
# --- DATI BASE ---
T = get_val(l_temp[idx], 0)
App = get_val(l_app[idx], 0)
Rh = int(get_val(l_rh[idx], 50))
t_suffix = ""
diff = App - T
if diff <= -2.5: t_suffix = "W"
elif diff >= 2.5: t_suffix = "H"
t_s = f"{int(round(T))}{t_suffix}"
Pr = get_val(l_prec[i], 0)
Sn = get_val(l_snow[i], 0)
Code = int(l_code[i]) if l_code[i] is not None else 0
p_suffix = ""
if Code in [96, 99]: p_suffix = "G"
elif Code in [66, 67]: p_suffix = "Z"
elif Sn > 0 or Code in [71, 73, 75, 77, 85, 86]: p_suffix = "N"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}{p_suffix}"
Pr = get_val(l_prec[idx], 0)
Sn = get_val(l_snow[idx], 0)
Code = int(get_val(l_code[idx], 0))
Rain = get_val(l_rain[idx], 0)
# Determina se è neve
is_snowing = Sn > 0 or (Code in [71, 73, 75, 77, 85, 86])
# Formattazione MM
p_suffix = ""
if Code in [96, 99]: p_suffix = "G"
elif Code in [66, 67]: p_suffix = "Z"
elif is_snowing and Pr >= 0.2: p_suffix = "N"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}{p_suffix}"
# --- CLOUD LOGIC (V10.4: LOCAL PRIORITY) ---
# Usiamo la MEDIA per la quantità (Panoramica)
c_avg_tot = int(avg_cl_tot[i])
# Usiamo i dati LOCALI per il tipo (Cosa ho sulla testa)
loc_L = get_val(l_cl_low_loc[i])
loc_M = get_val(l_cl_mid_loc[i])
loc_H = get_val(l_cl_hig_loc[i])
Vis = get_val(l_vis[i], 10000)
# --- CLOUD LOGIC ---
Cl = int(get_val(l_cl_tot_loc[idx], 0))
Vis = get_val(l_vis[idx], 10000)
# Calcola tipo nuvole per get_icon_set (L/M/H/F)
loc_L = get_val(l_cl_low_loc[idx])
loc_M = get_val(l_cl_mid_loc[idx])
loc_H = get_val(l_cl_hig_loc[idx])
types = {'L': loc_L, 'M': loc_M, 'H': loc_H}
dominant_type = max(types, key=types.get)
# Override: Se nubi basse locali > 40%, vincono loro
if loc_L > 40:
dominant_type = 'L'
# Step 1: Default matematico LOCALE
types = {'L': loc_L, 'M': loc_M, 'H': loc_H}
dominant_type = max(types, key=types.get)
# Quantità da mostrare: Media Globale
Cl = c_avg_tot
# Step 2: Override Tattico LOCALE
# Se LOCALMENTE le basse sono > 40%, vincono loro.
# (Soglia abbassata a 40 per catturare il 51%)
if loc_L > 40:
dominant_type = 'L'
# Se localmente è nuvoloso basso, forziamo la copertura visiva alta
# anche se la media globale è più bassa
if Cl < loc_L: Cl = int(loc_L)
# Nebbia
is_fog = False
if Vis < 1500:
is_fog = True
elif Code in [45, 48]:
is_fog = True
if is_fog:
dominant_type = 'F'
# Formattazione Nv%
if is_fog:
cl_str = "FOG"
else:
cl_str = f"{Cl}"
# Step 3: Nebbia (F)
is_fog = False
if Vis < 2000 or Code in [45, 48]:
is_fog = True
elif Rh >= 96 and loc_L > 40:
is_fog = True
if is_fog:
dominant_type = 'F'
if Cl < 100: Cl = 100
UV = get_val(l_uv[idx], 0)
uv_suffix = ""
if UV >= 10: uv_suffix = "E"
elif UV >= 7: uv_suffix = "H"
# Check varianza spaziale
min_p = min(points_cl_tot[i])
max_p = max(points_cl_tot[i])
var_symbol = ""
if (max_p - min_p) > 20:
var_symbol = "~"
# --- VENTO ---
Wspd = get_val(l_wspd[idx], 0)
Gust = get_val(l_gust[idx], 0)
Wdir = int(get_val(l_wdir[idx], 0))
Cape = get_val(l_cape[idx], 0)
IsDay = int(get_val(l_day[idx], 1))
card = degrees_to_cardinal(Wdir)
w_val = Gust if (Gust - Wspd) > 15 else Wspd
w_txt = f"{card} {int(round(w_val))}"
if (Gust - Wspd) > 15:
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}"
# --- ICONE ---
sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rain, Gust, Cape, dominant_type)
# Se c'è precipitazione nevosa, mostra ❄️ nella colonna Sk (invece di 🌨️)
if is_snowing and Pr >= 0.2:
sky = "❄️"
sky_fmt = f"{sky}{uv_suffix}"
cl_str = f"{var_symbol}{Cl}{dominant_type}"
UV = get_val(l_uv[i], 0)
uv_suffix = ""
if UV >= 10: uv_suffix = "E"
elif UV >= 7: uv_suffix = "H"
Wspd = get_val(l_wspd[i], 0)
Gust = get_val(l_gust[i], 0)
Wdir = int(get_val(l_wdir[i], 0))
Cape = get_val(l_cape[i], 0)
IsDay = int(l_day[i]) if l_day[i] is not None else 1
card = degrees_to_cardinal(Wdir)
w_val = Gust if (Gust - Wspd) > 15 else Wspd
w_txt = f"{card} {int(round(w_val))}"
if (Gust - Wspd) > 15:
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, get_val(l_rain[i], 0), Gust, Cape, dominant_type)
sky_fmt = f"{sky}{uv_suffix}"
lines.append(f"{dt.strftime('%H'):<2} {t_s:>4} {Rh:>3} {p_s:>3} {w_fmt} {cl_str:>5} {sky_fmt:<2} {sgx:<2}")
count += 1
except Exception as e:
logger.error(f"Errore riga meteo {i}: {e}")
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
current_block_lines.append(f"{dt.strftime('%H'):<2} {t_s:>4} {Rh:>3} {p_s:>3} {w_fmt} {cl_str:>5} {sky_fmt:<2} {sgx:<2}")
hours_from_start += 1
# Chiudi ultimo blocco (solo se ha contenuto oltre header e separator)
if current_block_lines and len(current_block_lines) > 2: # Header + separator + almeno 1 riga dati
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][current_day.weekday()]} {current_day.day}"
blocks.append(f"*{day_label}*\n```text\n" + "\n".join(current_block_lines) + "\n```")
if not blocks:
return f"❌ Nessun dato da mostrare nelle prossime 48 ore (da {current_hour.strftime('%H:%M')})."
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks)
@@ -337,16 +603,41 @@ if __name__ == "__main__":
args_parser.add_argument("--query", help="Nome città")
args_parser.add_argument("--home", action="store_true", help="Usa coordinate casa")
args_parser.add_argument("--debug", action="store_true", help="Mostra i valori dei 5 punti")
args_parser.add_argument("--chat_id", help="Chat ID Telegram per invio diretto (opzionale, può essere multiplo separato da virgola)")
args_parser.add_argument("--timezone", help="Timezone IANA (es: Europe/Rome, America/New_York)")
args = args_parser.parse_args()
# Determina chat_ids se specificato
chat_ids = None
if args.chat_id:
chat_ids = [cid.strip() for cid in args.chat_id.split(",") if cid.strip()]
# Genera report
report = None
if args.home:
print(generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, args.debug, "SM"))
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, args.debug, "SM")
elif args.query:
coords = get_coordinates(args.query)
if coords:
lat, lon, name, cc = coords
print(generate_weather_report(lat, lon, name, args.debug, cc))
report = generate_weather_report(lat, lon, name, args.debug, cc)
else:
print(f"❌ Città '{args.query}' non trovata.")
error_msg = f"❌ Città '{args.query}' non trovata."
if chat_ids:
telegram_send_markdown(error_msg, chat_ids)
else:
print(error_msg)
sys.exit(1)
else:
print("Uso: meteo.py --query 'Nome Città' oppure --home [--debug]")
usage_msg = "Uso: meteo.py --query 'Nome Città' oppure --home [--debug] [--chat_id ID]"
if chat_ids:
telegram_send_markdown(usage_msg, chat_ids)
else:
print(usage_msg)
sys.exit(1)
# Invia o stampa
if chat_ids:
telegram_send_markdown(report, chat_ids)
else:
print(report)