Files
loogle-scripts/services/telegram-bot/meteo.py

643 lines
25 KiB
Python

#!/usr/bin/env python3
import requests
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
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- CONFIGURAZIONE METEO ---
HOME_LAT = 43.9356
HOME_LON = 12.4296
HOME_NAME = "🏠 Casa (Wide View ±12km)"
TZ = "Europe/Berlin"
TZINFO = ZoneInfo(TZ)
# 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.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)
def parse_time(t: str) -> datetime.datetime:
try:
dt = date_parser.isoparse(t)
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
return dt.astimezone(TZINFO)
except Exception as e:
logger.error(f"Time parse error: {e}")
return now_local()
def degrees_to_cardinal(d: int) -> str:
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
try:
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 is_snowing else "🌧️"
else:
# 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 "🌙"
else: sky = "🌥️"
else:
if cloud <= 15: sky = "☀️" if is_day else "🌙"
elif cloud <= 35: sky = "🌤️" if is_day else "🌙"
elif cloud <= 60: sky = "⛅️"
elif cloud <= 85: sky = "🌥️"
else: sky = "☁️"
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 = ""
elif temp > 35: sgx = "🥵"
elif rain > 4: sgx = "☔️"
elif gust > 50: sgx = "💨"
return sky, sgx
except Exception as e:
logger.error(f"Icon error: {e}")
return "", "-"
def get_coordinates(city_name: 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 data["results"]:
res = data["results"][0]
cc = res.get("country_code", "IT").upper()
name = f"{res.get('name')} ({cc})"
return res["latitude"], res["longitude"], name, cc
except Exception as e:
logger.error(f"Geocoding error: {e}")
return None
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=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]
lat_str = ",".join(map(str, lats))
lon_str = ",".join(map(str, lons))
params = {
"latitude": lat_str, "longitude": lon_str, "timezone": tz_to_use,
"forecast_days": 3,
"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:
# 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:
error_details = f"Errore richiesta: {type(e).__name__}: {str(e)}"
logger.error(f"Request error: {error_details}")
return None, error_details
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
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", [])
if not times: return "❌ Dati orari mancanti."
L = len(times)
# --- DATI LOCALI (CASA) ---
l_temp = safe_get_list(hourly_c, "temperature_2m", L, 0)
l_app = safe_get_list(hourly_c, "apparent_temperature", L, 0)
l_rh = safe_get_list(hourly_c, "relative_humidity_2m", L, 50)
l_prec = safe_get_list(hourly_c, "precipitation", L, 0)
l_rain = safe_get_list(hourly_c, "rain", L, 0)
l_snow = safe_get_list(hourly_c, "snowfall", L, 0)
l_wspd = safe_get_list(hourly_c, "windspeed_10m", L, 0)
l_gust = safe_get_list(hourly_c, "windgusts_10m", L, 0)
l_wdir = safe_get_list(hourly_c, "winddirection_10m", L, 0)
l_code = safe_get_list(hourly_c, "weathercode", L, 0)
l_day = safe_get_list(hourly_c, "is_day", L, 1)
l_cape = safe_get_list(hourly_c, "cape", L, 0)
l_vis = safe_get_list(hourly_c, "visibility", L, 10000)
l_uv = safe_get_list(hourly_c, "uv_index", L, 0)
# 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 5 PUNTI) ---
acc_cl_tot = [0.0] * L
points_cl_tot = [ [] for _ in range(L) ]
for d in data_list:
h = d.get("hourly", {})
for i in range(L):
cc = get_val(safe_get_list(h, "cloud_cover", L)[i])
cl = get_val(safe_get_list(h, "cloud_cover_low", L)[i])
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
points_cl_tot[i].append(real_point_total)
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 METEO (v10.5)**\n"
now_h = now_local().replace(minute=0, second=0, microsecond=0)
idx = 0
for i, t_str in enumerate(times):
if parse_time(t_str) >= now_h:
idx = i
break
# 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)}% | 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 Nuvole**: {decision}\n"
return output
# --- 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)
# 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)
# 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[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 ---
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'
# 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}"
UV = get_val(l_uv[idx], 0)
uv_suffix = ""
if UV >= 10: uv_suffix = "E"
elif UV >= 7: uv_suffix = "H"
# --- 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}"
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)
if __name__ == "__main__":
args_parser = argparse.ArgumentParser()
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:
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
report = generate_weather_report(lat, lon, name, args.debug, cc)
else:
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:
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)