From 7594a428756f714a15588e1280316a954599055c Mon Sep 17 00:00:00 2001 From: daniele Date: Sun, 5 Apr 2026 07:00:02 +0200 Subject: [PATCH] Backup automatico script del 2026-04-05 07:00 --- services/telegram-bot/bot.py | 69 +++++++++++++++++++--------- services/telegram-bot/meteo.py | 57 +++++++++++++---------- services/telegram-bot/previsione7.py | 35 ++++++++++---- 3 files changed, 108 insertions(+), 53 deletions(-) diff --git a/services/telegram-bot/bot.py b/services/telegram-bot/bot.py index cbc6dd6..7b4ec77 100644 --- a/services/telegram-bot/bot.py +++ b/services/telegram-bot/bot.py @@ -153,24 +153,25 @@ 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: - return "UTC" + 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: """Aggiunge o aggiorna un viaggio attivo per un chat_id (sovrascrive se esiste)""" @@ -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,13 +757,36 @@ 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 @restricted async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: diff --git a/services/telegram-bot/meteo.py b/services/telegram-bot/meteo.py index 5fdc78d..8471c42 100644 --- a/services/telegram-bot/meteo.py +++ b/services/telegram-bot/meteo.py @@ -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: diff --git a/services/telegram-bot/previsione7.py b/services/telegram-bot/previsione7.py index f0c6dfd..9530d64 100755 --- a/services/telegram-bot/previsione7.py +++ b/services/telegram-bot/previsione7.py @@ -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: