Backup automatico script del 2026-04-05 07:00
This commit is contained in:
@@ -153,23 +153,24 @@ def get_timezone_from_coords(lat: float, lon: float) -> str:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Errore timezonefinder: {e}")
|
logger.warning(f"Errore timezonefinder: {e}")
|
||||||
|
|
||||||
# Fallback: stima timezone da longitudine (approssimativo)
|
# Fallback: stima da longitudine (approssimativo). Ordine importante: gli offset
|
||||||
# Ogni 15 gradi = 1 ora di differenza da UTC
|
# tipici delle Americhe (-4 NY, -7 Denver, …) rientrano in [-10, 2] e non devono
|
||||||
|
# essere classificati come Europa prima dei rami per le Americhe.
|
||||||
offset_hours = int(lon / 15)
|
offset_hours = int(lon / 15)
|
||||||
# Mappatura approssimativa a timezone IANA
|
if -8 <= offset_hours <= -6:
|
||||||
if -10 <= offset_hours <= 2: # Europa
|
|
||||||
return "Europe/Rome"
|
|
||||||
elif 3 <= offset_hours <= 5: # Medio Oriente
|
|
||||||
return "Asia/Dubai"
|
|
||||||
elif 6 <= offset_hours <= 8: # Asia centrale
|
|
||||||
return "Asia/Kolkata"
|
|
||||||
elif 9 <= offset_hours <= 11: # Asia orientale
|
|
||||||
return "Asia/Tokyo"
|
|
||||||
elif -5 <= offset_hours <= -3: # Americhe orientali
|
|
||||||
return "America/New_York"
|
|
||||||
elif -8 <= offset_hours <= -6: # Americhe occidentali
|
|
||||||
return "America/Los_Angeles"
|
return "America/Los_Angeles"
|
||||||
else:
|
if -5 <= offset_hours <= -3:
|
||||||
|
return "America/New_York"
|
||||||
|
if lon < -30 and offset_hours <= -9:
|
||||||
|
return "America/Los_Angeles"
|
||||||
|
if 3 <= offset_hours <= 5:
|
||||||
|
return "Asia/Dubai"
|
||||||
|
if 6 <= offset_hours <= 8:
|
||||||
|
return "Asia/Kolkata"
|
||||||
|
if 9 <= offset_hours <= 11:
|
||||||
|
return "Asia/Tokyo"
|
||||||
|
if -10 <= offset_hours <= 2:
|
||||||
|
return "Europe/Rome"
|
||||||
return "UTC"
|
return "UTC"
|
||||||
|
|
||||||
def add_viaggio(chat_id: str, location: str, lat: float, lon: float, name: str, timezone: Optional[str] = None) -> None:
|
def add_viaggio(chat_id: str, location: str, lat: float, lon: float, name: str, timezone: Optional[str] = None) -> None:
|
||||||
@@ -656,10 +657,11 @@ async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TY
|
|||||||
await update.message.reply_text(f"❌ Località '{location}' non trovata. Verifica il nome e riprova.", parse_mode="Markdown")
|
await update.message.reply_text(f"❌ Località '{location}' non trovata. Verifica il nome e riprova.", parse_mode="Markdown")
|
||||||
return
|
return
|
||||||
|
|
||||||
lat, lon, name, cc = coords
|
lat, lon, name, cc, geo_tz = coords
|
||||||
|
|
||||||
# Ottieni timezone per questa localizzazione
|
# Fuso: Open-Meteo geocoding espone già timezone IANA; altrimenti timezonefinder / fallback
|
||||||
timezone = get_timezone_from_coords(lat, lon)
|
geo_tz_clean = geo_tz.strip() if isinstance(geo_tz, str) and geo_tz.strip() else ""
|
||||||
|
timezone = geo_tz_clean or get_timezone_from_coords(lat, lon)
|
||||||
|
|
||||||
# Conferma riconoscimento località
|
# Conferma riconoscimento località
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
@@ -755,11 +757,34 @@ async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TY
|
|||||||
await update.message.reply_text(f"❌ Errore durante l'elaborazione: {str(e)}", parse_mode="Markdown")
|
await update.message.reply_text(f"❌ Errore durante l'elaborazione: {str(e)}", parse_mode="Markdown")
|
||||||
|
|
||||||
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
# Esegui una sola chiamata e invia il report a tutti i chat_id
|
# Stesso comportamento di `/meteo` senza argomenti: Casa (+ viaggio se attivo) per utente.
|
||||||
report = call_meteo_script(["--home"])
|
report_casa = call_meteo_script(["--home"])
|
||||||
for uid in ALLOWED_IDS:
|
for uid in ALLOWED_IDS:
|
||||||
|
chat_id = str(uid)
|
||||||
try:
|
try:
|
||||||
await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
|
await context.bot.send_message(
|
||||||
|
chat_id=uid,
|
||||||
|
text=f"🏠 **Report Meteo - Casa**\n\n{report_casa}",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
viaggio_attivo = get_viaggio(chat_id)
|
||||||
|
if viaggio_attivo:
|
||||||
|
report_viaggio = call_meteo_script(
|
||||||
|
[
|
||||||
|
"--query",
|
||||||
|
viaggio_attivo["location"],
|
||||||
|
"--timezone",
|
||||||
|
viaggio_attivo.get("timezone", "Europe/Rome"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=uid,
|
||||||
|
text=f"✈️ **Report Meteo - {viaggio_attivo['name']}**\n\n{report_viaggio}",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -90,14 +90,17 @@ def telegram_send_markdown(message_md: str, chat_ids: Optional[List[str]] = None
|
|||||||
def now_local() -> datetime.datetime:
|
def now_local() -> datetime.datetime:
|
||||||
return datetime.datetime.now(TZINFO)
|
return datetime.datetime.now(TZINFO)
|
||||||
|
|
||||||
def parse_time(t: str) -> datetime.datetime:
|
def parse_time(t: str, tz: Optional[ZoneInfo] = None) -> datetime.datetime:
|
||||||
|
"""Interpreta un timestamp ISO dell'API nel fuso richiesto (default: Casa / Europe/Berlin)."""
|
||||||
|
target = tz if tz is not None else TZINFO
|
||||||
try:
|
try:
|
||||||
dt = date_parser.isoparse(t)
|
dt = date_parser.isoparse(t)
|
||||||
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
|
if dt.tzinfo is None:
|
||||||
return dt.astimezone(TZINFO)
|
return dt.replace(tzinfo=target)
|
||||||
|
return dt.astimezone(target)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Time parse error: {e}")
|
logger.error(f"Time parse error: {e}")
|
||||||
return now_local()
|
return datetime.datetime.now(target)
|
||||||
|
|
||||||
def degrees_to_cardinal(d: int) -> str:
|
def degrees_to_cardinal(d: int) -> str:
|
||||||
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||||
@@ -164,7 +167,8 @@ def get_coordinates(city_name: str):
|
|||||||
res = data["results"][0]
|
res = data["results"][0]
|
||||||
cc = res.get("country_code", "IT").upper()
|
cc = res.get("country_code", "IT").upper()
|
||||||
name = f"{res.get('name')} ({cc})"
|
name = f"{res.get('name')} ({cc})"
|
||||||
return res["latitude"], res["longitude"], name, cc
|
geo_tz = res.get("timezone")
|
||||||
|
return res["latitude"], res["longitude"], name, cc, geo_tz
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Geocoding error: {e}")
|
logger.error(f"Geocoding error: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -301,23 +305,23 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
# Determina se è Casa
|
# Determina se è Casa
|
||||||
is_home = (abs(lat - HOME_LAT) < 0.01 and abs(lon - HOME_LON) < 0.01)
|
is_home = (abs(lat - HOME_LAT) < 0.01 and abs(lon - HOME_LON) < 0.01)
|
||||||
|
|
||||||
# Usa timezone personalizzata se fornita, altrimenti default
|
# Fuso per l'API: Casa = TZ; località = timezone esplicito/geocoding, altrimenti "auto" (Open-Meteo risolve da lat/lon)
|
||||||
tz_to_use = timezone if timezone else TZ
|
tz_for_api = timezone if timezone else (TZ if is_home else "auto")
|
||||||
|
|
||||||
model_id, model_name = choose_best_model(lat, lon, cc, is_home=is_home)
|
model_id, model_name = choose_best_model(lat, lon, cc, is_home=is_home)
|
||||||
|
|
||||||
# Tentativo 1: Richiesta iniziale
|
# 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)
|
data_list, error_details = get_forecast(lat, lon, model_id, is_home=is_home, timezone=tz_for_api, retry_after_60s=False)
|
||||||
|
|
||||||
# Se fallisce e siamo a Casa con ICON Italia, prova retry dopo 10 secondi
|
# Se fallisce e siamo a Casa con ICON Italia, prova retry dopo 10 secondi
|
||||||
if not data_list and is_home and model_id == "italia_meteo_arpae_icon_2i":
|
if not data_list and is_home and model_id == "italia_meteo_arpae_icon_2i":
|
||||||
logger.warning(f"Primo tentativo ICON Italia fallito: {error_details}. Retry dopo 10 secondi...")
|
logger.warning(f"Primo tentativo ICON Italia 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)
|
data_list, error_details = get_forecast(lat, lon, model_id, is_home=is_home, timezone=tz_for_api, retry_after_60s=True)
|
||||||
|
|
||||||
# Se ancora fallisce e siamo a Casa, fallback a best match
|
# Se ancora fallisce e siamo a Casa, fallback a best match
|
||||||
if not data_list and is_home:
|
if not data_list and is_home:
|
||||||
logger.warning(f"ICON Italia fallito anche dopo retry: {error_details}. Fallback a best match...")
|
logger.warning(f"ICON Italia 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)
|
data_list, error_details = get_forecast(lat, lon, None, is_home=False, timezone=tz_for_api, retry_after_60s=False)
|
||||||
if data_list:
|
if data_list:
|
||||||
model_name = "Best Match (fallback)"
|
model_name = "Best Match (fallback)"
|
||||||
logger.info("Fallback a best match riuscito")
|
logger.info("Fallback a best match riuscito")
|
||||||
@@ -332,6 +336,19 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
if not isinstance(data_list, list): data_list = [data_list]
|
if not isinstance(data_list, list): data_list = [data_list]
|
||||||
|
|
||||||
data_center = data_list[0]
|
data_center = data_list[0]
|
||||||
|
# Ora e giorno LT: fuso della località (non San Marino se non è Casa)
|
||||||
|
if timezone is not None:
|
||||||
|
tz_to_use = timezone
|
||||||
|
elif is_home:
|
||||||
|
tz_to_use = TZ
|
||||||
|
else:
|
||||||
|
tz_to_use = data_center.get("timezone") or TZ
|
||||||
|
try:
|
||||||
|
tz_to_use_info = ZoneInfo(tz_to_use)
|
||||||
|
except Exception:
|
||||||
|
tz_to_use_info = TZINFO
|
||||||
|
tz_to_use = TZ
|
||||||
|
|
||||||
hourly_c = data_center.get("hourly", {})
|
hourly_c = data_center.get("hourly", {})
|
||||||
times = hourly_c.get("time", [])
|
times = hourly_c.get("time", [])
|
||||||
if not times: return "❌ Dati orari mancanti."
|
if not times: return "❌ Dati orari mancanti."
|
||||||
@@ -381,10 +398,10 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
# --- DEBUG MODE ---
|
# --- DEBUG MODE ---
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
output = f"🔍 **DEBUG METEO (v10.5)**\n"
|
output = f"🔍 **DEBUG METEO (v10.5)**\n"
|
||||||
now_h = now_local().replace(minute=0, second=0, microsecond=0)
|
now_h = datetime.datetime.now(tz_to_use_info).replace(minute=0, second=0, microsecond=0)
|
||||||
idx = 0
|
idx = 0
|
||||||
for i, t_str in enumerate(times):
|
for i, t_str in enumerate(times):
|
||||||
if parse_time(t_str) >= now_h:
|
if parse_time(t_str, tz_to_use_info) >= now_h:
|
||||||
idx = i
|
idx = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -393,7 +410,7 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
loc_H = get_val(l_cl_hig_loc[idx])
|
loc_H = get_val(l_cl_hig_loc[idx])
|
||||||
code_now = int(get_val(l_code[idx]))
|
code_now = int(get_val(l_code[idx]))
|
||||||
|
|
||||||
output += f"Ora: {parse_time(times[idx]).strftime('%H:%M')}\n"
|
output += f"Ora: {parse_time(times[idx], tz_to_use_info).strftime('%H:%M')} (LT)\n"
|
||||||
output += f"📍 **LOCALE**: L:{int(loc_L)}% | H:{int(loc_H)}%\n"
|
output += f"📍 **LOCALE**: L:{int(loc_L)}% | H:{int(loc_H)}%\n"
|
||||||
output += f"☁️ **Nv%**: {int(avg_cl_tot[idx])}%\n"
|
output += f"☁️ **Nv%**: {int(avg_cl_tot[idx])}%\n"
|
||||||
output += f"❄️ **NEVE**: Codice={code_now}, Accumulo={get_val(l_snow[idx])}cm\n"
|
output += f"❄️ **NEVE**: Codice={code_now}, Accumulo={get_val(l_snow[idx])}cm\n"
|
||||||
@@ -404,8 +421,6 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
# --- GENERAZIONE TABELLA ---
|
# --- 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)
|
now_local_tz = datetime.datetime.now(tz_to_use_info)
|
||||||
|
|
||||||
# Inizia dall'ora corrente (arrotondata all'ora)
|
# Inizia dall'ora corrente (arrotondata all'ora)
|
||||||
@@ -418,12 +433,7 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
|
|||||||
valid_indices = []
|
valid_indices = []
|
||||||
for i, t_str in enumerate(times):
|
for i, t_str in enumerate(times):
|
||||||
try:
|
try:
|
||||||
dt = parse_time(t_str)
|
dt = parse_time(t_str, tz_to_use_info)
|
||||||
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
|
# Include solo timestamp >= current_hour e < end_hour
|
||||||
if current_hour <= dt < end_hour:
|
if current_hour <= dt < end_hour:
|
||||||
valid_indices.append((i, dt))
|
valid_indices.append((i, dt))
|
||||||
@@ -608,8 +618,9 @@ if __name__ == "__main__":
|
|||||||
elif args.query:
|
elif args.query:
|
||||||
coords = get_coordinates(args.query)
|
coords = get_coordinates(args.query)
|
||||||
if coords:
|
if coords:
|
||||||
lat, lon, name, cc = coords
|
lat, lon, name, cc, geo_tz = coords
|
||||||
report = generate_weather_report(lat, lon, name, args.debug, cc)
|
tz = args.timezone or geo_tz
|
||||||
|
report = generate_weather_report(lat, lon, name, args.debug, cc, timezone=tz)
|
||||||
else:
|
else:
|
||||||
error_msg = f"❌ Città '{args.query}' non trovata."
|
error_msg = f"❌ Città '{args.query}' non trovata."
|
||||||
if chat_ids:
|
if chat_ids:
|
||||||
|
|||||||
@@ -365,6 +365,15 @@ def _merge_hourly_median(hourly_by_model, single_source_keys=None, single_source
|
|||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
out[key].append(_median_or_single(vals) if vals else None)
|
out[key].append(_median_or_single(vals) if vals else None)
|
||||||
|
n = len(out["time"])
|
||||||
|
if n > 1:
|
||||||
|
order = sorted(range(n), key=lambda i: str(out["time"][i]))
|
||||||
|
out["time"] = [out["time"][i] for i in order]
|
||||||
|
for key in all_keys:
|
||||||
|
if key == "time":
|
||||||
|
continue
|
||||||
|
if len(out.get(key, [])) == n:
|
||||||
|
out[key] = [out[key][i] for i in order]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -420,6 +429,16 @@ def _merge_daily_median(daily_by_model, single_source_keys=None, single_source_m
|
|||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
out[key].append(_median_or_single(vals) if vals else None)
|
out[key].append(_median_or_single(vals) if vals else None)
|
||||||
|
# Ordina cronologicamente (evita buchi nel report se l'unione non era ordinata)
|
||||||
|
n = len(out["time"])
|
||||||
|
if n > 1:
|
||||||
|
order = sorted(range(n), key=lambda i: out["time"][i])
|
||||||
|
out["time"] = [out["time"][i] for i in order]
|
||||||
|
for key in all_keys:
|
||||||
|
if key == "time":
|
||||||
|
continue
|
||||||
|
if len(out.get(key, [])) == n:
|
||||||
|
out[key] = [out[key][i] for i in order]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -544,13 +563,14 @@ def merge_multi_model_forecast(models_data, forecast_days=10):
|
|||||||
long_daily_times = merged_long_daily.get("time") or []
|
long_daily_times = merged_long_daily.get("time") or []
|
||||||
long_hourly_times = merged_long_hourly.get("time") or []
|
long_hourly_times = merged_long_hourly.get("time") or []
|
||||||
names_long = " + ".join(MODEL_NAMES.get(m, m) for m, _ in long_term_list[:3])
|
names_long = " + ".join(MODEL_NAMES.get(m, m) for m, _ in long_term_list[:3])
|
||||||
merged["models_used"].append(f"{names_long} (mediana) ({cutoff_day+1}-{forecast_days}d)")
|
# Allinea al numero effettivo di giorni/orari short (non indice fisso): evita buco del 3° giorno
|
||||||
start_idx = cutoff_day + 1
|
start_idx = len(merged["daily"]["time"])
|
||||||
|
start_hour_idx = len(merged["hourly"]["time"])
|
||||||
|
merged["models_used"].append(f"{names_long} (mediana) (giorno {start_idx + 1}-{forecast_days}d)")
|
||||||
for i, day_time in enumerate(long_daily_times):
|
for i, day_time in enumerate(long_daily_times):
|
||||||
day_num = i
|
if i < start_idx:
|
||||||
if day_num < start_idx:
|
|
||||||
continue
|
continue
|
||||||
if day_num >= forecast_days:
|
if i >= forecast_days:
|
||||||
break
|
break
|
||||||
merged["daily"]["time"].append(day_time)
|
merged["daily"]["time"].append(day_time)
|
||||||
for key in daily_keys:
|
for key in daily_keys:
|
||||||
@@ -558,7 +578,6 @@ def merge_multi_model_forecast(models_data, forecast_days=10):
|
|||||||
continue
|
continue
|
||||||
arr = merged_long_daily.get(key, [])
|
arr = merged_long_daily.get(key, [])
|
||||||
merged["daily"][key].append(arr[i] if i < len(arr) else None)
|
merged["daily"][key].append(arr[i] if i < len(arr) else None)
|
||||||
start_hour_idx = (cutoff_day + 1) * 24
|
|
||||||
needed_hours = forecast_days * 24
|
needed_hours = forecast_days * 24
|
||||||
for i in range(start_hour_idx, min(len(long_hourly_times), needed_hours)):
|
for i in range(start_hour_idx, min(len(long_hourly_times), needed_hours)):
|
||||||
merged["hourly"]["time"].append(long_hourly_times[i])
|
merged["hourly"]["time"].append(long_hourly_times[i])
|
||||||
@@ -571,9 +590,9 @@ def merge_multi_model_forecast(models_data, forecast_days=10):
|
|||||||
long_term_model, long_term_data = long_term_list[0]
|
long_term_model, long_term_data = long_term_list[0]
|
||||||
long_daily = long_term_data.get("daily", {}) or {}
|
long_daily = long_term_data.get("daily", {}) or {}
|
||||||
long_hourly = long_term_data.get("hourly", {}) or {}
|
long_hourly = long_term_data.get("hourly", {}) or {}
|
||||||
merged["models_used"].append(f"{MODEL_NAMES.get(long_term_model, long_term_model)} ({cutoff_day+1}-{forecast_days}d)")
|
start_idx = len(merged["daily"]["time"])
|
||||||
|
merged["models_used"].append(f"{MODEL_NAMES.get(long_term_model, long_term_model)} (giorno {start_idx + 1}-{forecast_days}d)")
|
||||||
long_daily_times = long_daily.get("time", []) or []
|
long_daily_times = long_daily.get("time", []) or []
|
||||||
start_idx = cutoff_day + 1
|
|
||||||
for i in range(start_idx, min(len(long_daily_times), forecast_days)):
|
for i in range(start_idx, min(len(long_daily_times), forecast_days)):
|
||||||
merged["daily"]["time"].append(long_daily_times[i])
|
merged["daily"]["time"].append(long_daily_times[i])
|
||||||
for key in daily_keys:
|
for key in daily_keys:
|
||||||
|
|||||||
Reference in New Issue
Block a user