Backup automatico script del 2026-04-05 07:00

This commit is contained in:
2026-04-05 07:00:02 +02:00
parent c25c309a15
commit 7594a42875
3 changed files with 108 additions and 53 deletions
+46 -21
View File
@@ -153,23 +153,24 @@ def get_timezone_from_coords(lat: float, lon: float) -> str:
except Exception as e:
logger.warning(f"Errore timezonefinder: {e}")
# Fallback: stima timezone da longitudine (approssimativo)
# Ogni 15 gradi = 1 ora di differenza da UTC
# Fallback: stima da longitudine (approssimativo). Ordine importante: gli offset
# 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)
# Mappatura approssimativa a timezone IANA
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
if -8 <= offset_hours <= -6:
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"
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")
return
lat, lon, name, cc = coords
lat, lon, name, cc, geo_tz = coords
# Ottieni timezone per questa localizzazione
timezone = get_timezone_from_coords(lat, lon)
# Fuso: Open-Meteo geocoding espone già timezone IANA; altrimenti timezonefinder / fallback
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à
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")
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
# Esegui una sola chiamata e invia il report a tutti i chat_id
report = call_meteo_script(["--home"])
# Stesso comportamento di `/meteo` senza argomenti: Casa (+ viaggio se attivo) per utente.
report_casa = call_meteo_script(["--home"])
for uid in ALLOWED_IDS:
chat_id = str(uid)
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:
pass
+34 -23
View File
@@ -90,14 +90,17 @@ def telegram_send_markdown(message_md: str, chat_ids: Optional[List[str]] = None
def now_local() -> datetime.datetime:
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:
dt = date_parser.isoparse(t)
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
return dt.astimezone(TZINFO)
if dt.tzinfo is None:
return dt.replace(tzinfo=target)
return dt.astimezone(target)
except Exception as e:
logger.error(f"Time parse error: {e}")
return now_local()
return datetime.datetime.now(target)
def degrees_to_cardinal(d: int) -> str:
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
@@ -164,7 +167,8 @@ def get_coordinates(city_name: str):
res = data["results"][0]
cc = res.get("country_code", "IT").upper()
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:
logger.error(f"Geocoding error: {e}")
return None
@@ -301,23 +305,23 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
# 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
# Fuso per l'API: Casa = TZ; località = timezone esplicito/geocoding, altrimenti "auto" (Open-Meteo risolve da lat/lon)
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)
# 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
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...")
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
if not data_list and is_home:
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:
model_name = "Best Match (fallback)"
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]
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", {})
times = hourly_c.get("time", [])
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 ---
if debug_mode:
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
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
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])
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"☁️ **Nv%**: {int(avg_cl_tot[idx])}%\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
# --- 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)
@@ -418,12 +433,7 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT",
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)
dt = parse_time(t_str, tz_to_use_info)
# Include solo timestamp >= current_hour e < end_hour
if current_hour <= dt < end_hour:
valid_indices.append((i, dt))
@@ -608,8 +618,9 @@ if __name__ == "__main__":
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)
lat, lon, name, cc, geo_tz = coords
tz = args.timezone or geo_tz
report = generate_weather_report(lat, lon, name, args.debug, cc, timezone=tz)
else:
error_msg = f"❌ Città '{args.query}' non trovata."
if chat_ids:
+27 -8
View File
@@ -365,6 +365,15 @@ def _merge_hourly_median(hourly_by_model, single_source_keys=None, single_source
pass
break
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
@@ -420,6 +429,16 @@ def _merge_daily_median(daily_by_model, single_source_keys=None, single_source_m
pass
break
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
@@ -544,13 +563,14 @@ def merge_multi_model_forecast(models_data, forecast_days=10):
long_daily_times = merged_long_daily.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])
merged["models_used"].append(f"{names_long} (mediana) ({cutoff_day+1}-{forecast_days}d)")
start_idx = cutoff_day + 1
# Allinea al numero effettivo di giorni/orari short (non indice fisso): evita buco del 3° giorno
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):
day_num = i
if day_num < start_idx:
if i < start_idx:
continue
if day_num >= forecast_days:
if i >= forecast_days:
break
merged["daily"]["time"].append(day_time)
for key in daily_keys:
@@ -558,7 +578,6 @@ def merge_multi_model_forecast(models_data, forecast_days=10):
continue
arr = merged_long_daily.get(key, [])
merged["daily"][key].append(arr[i] if i < len(arr) else None)
start_hour_idx = (cutoff_day + 1) * 24
needed_hours = forecast_days * 24
for i in range(start_hour_idx, min(len(long_hourly_times), needed_hours)):
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_daily = long_term_data.get("daily", {}) 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 []
start_idx = cutoff_day + 1
for i in range(start_idx, min(len(long_daily_times), forecast_days)):
merged["daily"]["time"].append(long_daily_times[i])
for key in daily_keys: