From f0c567260768943b1db14f4b088f039ee1e1ce5c Mon Sep 17 00:00:00 2001 From: daniele Date: Sun, 25 Jan 2026 07:00:03 +0100 Subject: [PATCH] Backup automatico script del 2026-01-25 07:00 --- scripts/pi2-backup/super_watchdog.sh | 3 +- services/telegram-bot/arome_snow_alert.py | 14 +-- services/telegram-bot/bot.py | 18 +++- services/telegram-bot/check_ghiaccio.py | 73 ++++++++++++--- services/telegram-bot/freeze_alert.py | 3 +- services/telegram-bot/meteo.py | 9 +- services/telegram-bot/net_quality.py | 20 +++- services/telegram-bot/nowcast_120m_alert.py | 13 +-- services/telegram-bot/open_meteo_client.py | 92 +++++++++++++++++++ services/telegram-bot/previsione7.py | 9 +- services/telegram-bot/road_weather.py | 5 +- services/telegram-bot/severe_weather.py | 11 ++- .../severe_weather_circondario.py | 3 +- .../telegram-bot/smart_irrigation_advisor.py | 11 ++- services/telegram-bot/snow_radar.py | 4 +- services/telegram-bot/student_alert.py | 14 +-- 16 files changed, 241 insertions(+), 61 deletions(-) mode change 100644 => 100755 scripts/pi2-backup/super_watchdog.sh create mode 100644 services/telegram-bot/open_meteo_client.py diff --git a/scripts/pi2-backup/super_watchdog.sh b/scripts/pi2-backup/super_watchdog.sh old mode 100644 new mode 100755 index 2e84d81..f8dfec1 --- a/scripts/pi2-backup/super_watchdog.sh +++ b/scripts/pi2-backup/super_watchdog.sh @@ -36,9 +36,8 @@ TARGETS=( # INFRASTRUTTURA 🔌 "🗄️ NAS DS214|192.168.128.90" "🔌 Switch Sala (.105)|192.168.128.105" - "🔌 Switch Main (.106)|192.168.128.106" + "🔌 Switch Taverna (.106)|192.168.128.106" "🔌 Switch Lavanderia (.107)|192.168.128.107" - "🔌 Switch Taverna (.108)|192.168.128.108" # WIFI 📶 "📶 WiFi Sala (.101)|192.168.128.101" diff --git a/services/telegram-bot/arome_snow_alert.py b/services/telegram-bot/arome_snow_alert.py index 6a0ff21..b6ce6bb 100644 --- a/services/telegram-bot/arome_snow_alert.py +++ b/services/telegram-bot/arome_snow_alert.py @@ -15,6 +15,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import configure_open_meteo_session # ============================================================================= # arome_snow_alert.py @@ -382,7 +383,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str, params["minutely_15"] = "snowfall,precipitation_probability,precipitation,rain,temperature_2m" try: - r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 25)) if r.status_code == 400: # Se 400 e abbiamo minutely_15, riprova senza if "minutely_15" in params and model == MODEL_AROME: @@ -390,7 +391,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str, params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -408,7 +409,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str, params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -460,7 +461,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str, params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -474,7 +475,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str, params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -488,7 +489,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str, params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -1399,6 +1400,7 @@ def analyze_snow(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) casa_location_name = "🏠 Casa" with requests.Session() as session: + configure_open_meteo_session(session, headers=HTTP_HEADERS) for p in POINTS: # Recupera AROME seamless data_arome = get_forecast(session, p["lat"], p["lon"], MODEL_AROME) diff --git a/services/telegram-bot/bot.py b/services/telegram-bot/bot.py index a2a0acd..e29b527 100644 --- a/services/telegram-bot/bot.py +++ b/services/telegram-bot/bot.py @@ -5,6 +5,7 @@ import datetime import requests import shlex import json +import time from functools import wraps from typing import Optional from zoneinfo import ZoneInfo @@ -58,9 +59,8 @@ INFRA_DEVICES = [ {"name": "📶 WiFi Taverna", "ip": "192.168.128.103"}, {"name": "📶 WiFi Dado", "ip": "192.168.128.104"}, {"name": "🔌 Sw Sala", "ip": "192.168.128.105"}, - {"name": "🔌 Sw Main", "ip": "192.168.128.106"}, - {"name": "🔌 Sw Lav", "ip": "192.168.128.107"}, - {"name": "🔌 Sw Tav", "ip": "192.168.128.108"} + {"name": "🔌 Sw Tav", "ip": "192.168.128.106"}, + {"name": "🔌 Sw Lav", "ip": "192.168.128.107"} ] logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) @@ -84,9 +84,17 @@ def get_ping_icon(ip): except Exception: return "🔴" def get_device_stats(device): - ip, user, dtype = device['ip'], device['type'], device['user'] + ip, user, dtype = device['ip'], device['user'], device['type'] uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) - if not uptime_raw or "Err" in uptime_raw: return "🔴 **OFFLINE**" + if not uptime_raw or "Err" in uptime_raw: + # Retry once to reduce transient SSH hiccups. + time.sleep(0.5) + uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) + if not uptime_raw or "Err" in uptime_raw: + # If ping is OK but SSH failed, mark as online with warning. + if get_ping_icon(ip) == "✅": + return "🟡 **ONLINE (SSH non raggiungibile)**" + return "🔴 **OFFLINE**" uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0] temp = "N/A" if dtype in ["pi", "local"]: diff --git a/services/telegram-bot/check_ghiaccio.py b/services/telegram-bot/check_ghiaccio.py index ef26461..f69c204 100644 --- a/services/telegram-bot/check_ghiaccio.py +++ b/services/telegram-bot/check_ghiaccio.py @@ -1,5 +1,6 @@ import argparse import requests +from open_meteo_client import open_meteo_get import datetime import os import sys @@ -77,31 +78,68 @@ def get_bot_token(): sys.exit(1) -def save_current_state(state): +def save_current_state(state, report_meta=None): try: # Aggiungi timestamp corrente per tracciare quando è stato salvato lo stato + if report_meta is None: + report_meta = {} state_with_meta = { "points": state, - "last_update": datetime.datetime.now().isoformat() + "last_update": datetime.datetime.now().isoformat(), + "report_meta": report_meta, } with open(STATE_FILE, 'w') as f: json.dump(state_with_meta, f) except Exception as e: print(f"Errore salvataggio stato: {e}") -def load_previous_state(): +def load_state_with_meta(): if not os.path.exists(STATE_FILE): - return {} + return {}, {} try: with open(STATE_FILE, 'r') as f: data = json.load(f) # Supporta sia il formato vecchio (solo dict di punti) che nuovo (con metadata) if isinstance(data, dict) and "points" in data: - return data["points"] + return data.get("points", {}), data.get("report_meta", {}) else: - return data + return data, {} except Exception: - return {} + return {}, {} + + +def normalize_report_meta(report_meta: dict) -> dict: + today = datetime.date.today().isoformat() + date_str = report_meta.get("date") + count = report_meta.get("count", 0) + if date_str != today: + return {"date": today, "count": 0} + try: + count = int(count) + except Exception: + count = 0 + return {"date": today, "count": max(0, count)} + + +def is_important_update(new_level: int, old_level: int, message: str) -> bool: + # Importante se rischio alto (gelicidio) o neve su strada (livello 4). + if max(new_level, old_level) >= 3: + return True + lowered = (message or "").lower() + return "gelicidio" in lowered or "neve" in lowered + + +def append_report(target_list: list, message: str, important: bool, report_meta: dict, debug_mode: bool) -> None: + DAILY_LIMIT = 3 + if important: + target_list.append(message) + return + if report_meta.get("count", 0) >= DAILY_LIMIT: + if debug_mode: + print(" ⏸️ Report non importante saltato: limite giornaliero raggiunto") + return + target_list.append(message) + report_meta["count"] = report_meta.get("count", 0) + 1 def is_improvement_report_allowed() -> bool: """ @@ -151,7 +189,7 @@ def get_weather_data(lat, lon, model_slug, include_past_days=1): params["minutely_15"] = "temperature_2m,precipitation,rain,snowfall" try: - response = requests.get(url, params=params, timeout=15) + response = open_meteo_get(url, params=params, timeout=(5, 15)) response.raise_for_status() return response.json() except Exception as e: @@ -1121,7 +1159,7 @@ def get_coordinates_from_city(city_name: str) -> Optional[Tuple[float, float, st url = "https://geocoding-api.open-meteo.com/v1/search" try: - resp = requests.get(url, params={"name": city_name, "count": 1, "language": "it", "format": "json"}, timeout=5) + resp = open_meteo_get(url, params={"name": city_name, "count": 1, "language": "it", "format": "json"}, timeout=(5, 10)) res = resp.json().get("results", []) if res: res = res[0] @@ -1926,7 +1964,8 @@ def main(): DEBUG_MODE = args.debug token = get_bot_token() - previous_state = load_previous_state() + previous_state, report_meta = load_state_with_meta() + report_meta = normalize_report_meta(report_meta) current_state = {} new_alerts = [] @@ -2074,7 +2113,8 @@ def main(): for detail in past_24h_details: final_msg += f"{detail}\n" - new_alerts.append(final_msg) + important = is_important_update(max_risk_level, old_level, final_msg) + append_report(new_alerts, final_msg, important, report_meta, DEBUG_MODE) # 3. Rischio Cessato (Tutti i modelli danno verde) # IMPORTANTE: Non inviare "allerta rientrata" se ci sono ancora condizioni che mantengono il ghiaccio @@ -2129,7 +2169,8 @@ def main(): # Non aggiungere a solved_alerts - il ghiaccio potrebbe ancora essere presente # Ma potremmo inviare un messaggio informativo se in debug mode if DEBUG_MODE: - new_alerts.append(persist_msg) + important = is_important_update(max_risk_level, old_level, persist_msg) + append_report(new_alerts, persist_msg, important, report_meta, DEBUG_MODE) else: # Condizioni completamente risolte: neve sciolta e temperature sopra lo zero if DEBUG_MODE: @@ -2153,7 +2194,8 @@ def main(): solved_msg += f" (Scioglimento confermato: {', '.join(melting_info)})" else: solved_msg += " (Tutti i modelli)" - solved_alerts.append(solved_msg) + important = is_important_update(max_risk_level, old_level, solved_msg) + append_report(solved_alerts, solved_msg, important, report_meta, DEBUG_MODE) # 4. Rischio Diminuito (es. Da Ghiaccio a Brina, o da Brina a nessun rischio ma non ancora 0) elif max_risk_level < old_level and max_risk_level > 0: @@ -2191,7 +2233,8 @@ def main(): for detail in past_24h_details: improvement_msg += f"{detail}\n" - new_alerts.append(improvement_msg) + important = is_important_update(max_risk_level, old_level, improvement_msg) + append_report(new_alerts, improvement_msg, important, report_meta, DEBUG_MODE) # Genera e invia mappa solo quando ci sono aggiornamenti if new_alerts or solved_alerts: @@ -2231,7 +2274,7 @@ def main(): print("Nessuna variazione.") if not DEBUG_MODE: - save_current_state(current_state) + save_current_state(current_state, report_meta=report_meta) if __name__ == "__main__": main() diff --git a/services/telegram-bot/freeze_alert.py b/services/telegram-bot/freeze_alert.py index 1ee32c6..c53c23d 100644 --- a/services/telegram-bot/freeze_alert.py +++ b/services/telegram-bot/freeze_alert.py @@ -13,6 +13,7 @@ from typing import Dict, List, Optional, Tuple from zoneinfo import ZoneInfo import requests +from open_meteo_client import open_meteo_get from dateutil import parser # ============================================================================= @@ -236,7 +237,7 @@ def get_forecast() -> Optional[Dict]: "minutely_15": "temperature_2m", # Dettaglio 15 minuti per inizio preciso gelo } try: - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + r = open_meteo_get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 25)) if r.status_code == 400: try: j = r.json() diff --git a/services/telegram-bot/meteo.py b/services/telegram-bot/meteo.py index 36e7e1e..c233764 100644 --- a/services/telegram-bot/meteo.py +++ b/services/telegram-bot/meteo.py @@ -9,6 +9,7 @@ import time from typing import Optional, List from zoneinfo import ZoneInfo from dateutil import parser as date_parser +from open_meteo_client import open_meteo_get # Setup logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -161,7 +162,7 @@ def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape, c 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) + r = open_meteo_get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 15)) data = r.json() if "results" in data and data["results"]: res = data["results"][0] @@ -223,7 +224,7 @@ def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after try: t0 = time.time() # Timeout ridotto a 20s per fallire più velocemente in caso di problemi - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=20) + r = open_meteo_get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 20)) if r.status_code != 200: # Dettagli errore più specifici error_details = f"Status {r.status_code}" @@ -276,7 +277,7 @@ def get_visibility_forecast(lat, lon): try: t0 = time.time() # Timeout ridotto a 12s per fallire più velocemente - r = requests.get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=12) + r = open_meteo_get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=(5, 12)) if r.status_code == 200: data = r.json() hourly = data.get("hourly", {}) @@ -299,7 +300,7 @@ def get_visibility_forecast(lat, lon): try: t0 = time.time() # Timeout ridotto a 12s per fallire più velocemente - r = requests.get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=12) + r = open_meteo_get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=(5, 12)) if r.status_code == 200: data = r.json() hourly = data.get("hourly", {}) diff --git a/services/telegram-bot/net_quality.py b/services/telegram-bot/net_quality.py index d3b71b3..ab7722c 100644 --- a/services/telegram-bot/net_quality.py +++ b/services/telegram-bot/net_quality.py @@ -1,4 +1,5 @@ import argparse +import datetime import subprocess import re import os @@ -21,6 +22,16 @@ LIMIT_JITTER = 30.0 # ms di deviazione (sopra 30ms lagga la voce/gioco) # File di stato STATE_FILE = "/home/daniely/docker/telegram-bot/quality_state.json" +LOG_FILE = "/home/daniely/docker/telegram-bot/quality_log.txt" + + +def log_line(message: str) -> None: + ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"{ts} {message}\n") + except Exception: + pass def send_telegram(msg, chat_ids: Optional[List[str]] = None): """ @@ -55,6 +66,7 @@ def save_state(active): def measure_quality(chat_ids: Optional[List[str]] = None): print("--- Avvio Test Qualità Linea ---") + log_line("INFO Avvio Test Qualità Linea") # Esegue 50 ping rapidi (0.2s intervallo) # -q: quiet (solo riepilogo finale) @@ -89,7 +101,9 @@ def measure_quality(chat_ids: Optional[List[str]] = None): else: avg_ping = 0.0 - print(f"Risultati: Loss={loss}% | Jitter={jitter}ms | AvgPing={avg_ping}ms") + result_line = f"Risultati: Loss={loss}% | Jitter={jitter}ms | AvgPing={avg_ping}ms" + print(result_line) + log_line(f"INFO {result_line}") # --- LOGICA ALLARME --- state = load_state() @@ -110,8 +124,10 @@ def measure_quality(chat_ids: Optional[List[str]] = None): send_telegram(msg, chat_ids=chat_ids) save_state(True) print("Allarme inviato.") + log_line("WARN Allarme inviato") else: print("Qualità ancora scarsa (già notificato).") + log_line("WARN Qualità ancora scarsa (già notificato)") elif was_active and not is_bad: # RECOVERY @@ -121,8 +137,10 @@ def measure_quality(chat_ids: Optional[List[str]] = None): send_telegram(msg, chat_ids=chat_ids) save_state(False) print("Recovery inviata.") + log_line("INFO Recovery inviata") else: print("Linea OK.") + log_line("INFO Linea OK") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Network quality monitor") diff --git a/services/telegram-bot/nowcast_120m_alert.py b/services/telegram-bot/nowcast_120m_alert.py index be31a09..97cda41 100644 --- a/services/telegram-bot/nowcast_120m_alert.py +++ b/services/telegram-bot/nowcast_120m_alert.py @@ -13,6 +13,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import open_meteo_get # ========================= # CONFIG @@ -201,7 +202,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2) params["minutely_15"] = "snowfall,precipitation_probability,precipitation,rain,temperature_2m,wind_speed_10m,wind_direction_10m" try: - r = requests.get(OPEN_METEO_URL, params=params, timeout=25) + r = open_meteo_get(OPEN_METEO_URL, params=params, timeout=(5, 25)) if r.status_code == 400: # Se 400 e abbiamo minutely_15, riprova senza if "minutely_15" in params and model == MODEL_AROME: @@ -209,7 +210,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -227,7 +228,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -260,7 +261,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -274,7 +275,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -288,7 +289,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: diff --git a/services/telegram-bot/open_meteo_client.py b/services/telegram-bot/open_meteo_client.py new file mode 100644 index 0000000..f2ac106 --- /dev/null +++ b/services/telegram-bot/open_meteo_client.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Shared Open-Meteo HTTP client with retries and sane timeouts. +Use for all calls to Open-Meteo APIs to reduce transient timeouts/502s. +""" + +from __future__ import annotations + +from typing import Dict, Optional, Tuple + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +DEFAULT_TIMEOUT: Tuple[int, int] = (5, 25) # connect, read + +_SESSION_CACHE: dict[tuple, requests.Session] = {} + + +def _session_key(headers: Optional[Dict[str, str]], retries: int, backoff: float) -> tuple: + headers_key = tuple(sorted(headers.items())) if headers else () + return headers_key, retries, backoff + + +def get_open_meteo_session( + headers: Optional[Dict[str, str]] = None, + retries: int = 3, + backoff: float = 0.8, +) -> requests.Session: + key = _session_key(headers, retries, backoff) + if key in _SESSION_CACHE: + return _SESSION_CACHE[key] + + retry = Retry( + total=retries, + connect=retries, + read=retries, + status=retries, + status_forcelist=(429, 500, 502, 503, 504), + allowed_methods=frozenset(["GET"]), + backoff_factor=backoff, + raise_on_status=False, + respect_retry_after_header=True, + ) + adapter = HTTPAdapter(max_retries=retry) + session = requests.Session() + session.mount("https://", adapter) + session.mount("http://", adapter) + if headers: + session.headers.update(headers) + + _SESSION_CACHE[key] = session + return session + + +def configure_open_meteo_session( + session: requests.Session, + headers: Optional[Dict[str, str]] = None, + retries: int = 3, + backoff: float = 0.8, +) -> requests.Session: + retry = Retry( + total=retries, + connect=retries, + read=retries, + status=retries, + status_forcelist=(429, 500, 502, 503, 504), + allowed_methods=frozenset(["GET"]), + backoff_factor=backoff, + raise_on_status=False, + respect_retry_after_header=True, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount("https://", adapter) + session.mount("http://", adapter) + if headers: + session.headers.update(headers) + return session + + +def open_meteo_get( + url: str, + params: Optional[Dict] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Tuple[int, int] = DEFAULT_TIMEOUT, + retries: int = 3, + backoff: float = 0.8, +) -> requests.Response: + session = get_open_meteo_session(headers=headers, retries=retries, backoff=backoff) + return session.get(url, params=params, timeout=timeout) diff --git a/services/telegram-bot/previsione7.py b/services/telegram-bot/previsione7.py index bbfb6ec..fc0ba69 100755 --- a/services/telegram-bot/previsione7.py +++ b/services/telegram-bot/previsione7.py @@ -12,6 +12,7 @@ from zoneinfo import ZoneInfo from collections import defaultdict, Counter, Counter from typing import List, Dict, Tuple, Optional from statistics import mean, median +from open_meteo_client import open_meteo_get # --- CONFIGURAZIONE DEFAULT --- DEFAULT_LAT = 43.9356 @@ -94,7 +95,7 @@ def get_coordinates(query): return DEFAULT_LAT, DEFAULT_LON, DEFAULT_NAME, "SM" url = "https://geocoding-api.open-meteo.com/v1/search" try: - resp = requests.get(url, params={"name": query, "count": 1, "language": "it", "format": "json"}, timeout=5) + resp = open_meteo_get(url, params={"name": query, "count": 1, "language": "it", "format": "json"}, timeout=(5, 10)) res = resp.json().get("results", []) if res: res = res[0] @@ -131,7 +132,7 @@ def get_weather_multi_model(lat, lon, short_term_models, long_term_models, forec "timezone": timezone if timezone else TZ_STR, "forecast_days": min(forecast_days, 3) # Limita a 3 giorni per modelli ad alta risoluzione } try: - resp = requests.get(url, params=params, timeout=20) + resp = open_meteo_get(url, params=params, timeout=(5, 20)) if resp.status_code == 200: data = resp.json() # Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri) @@ -172,7 +173,7 @@ def get_weather_multi_model(lat, lon, short_term_models, long_term_models, forec "timezone": timezone if timezone else TZ_STR, "models": model, "forecast_days": min(forecast_days, 3) # Limita a 3 giorni per modelli ad alta risoluzione } try: - resp = requests.get(url, params=params, timeout=20) + resp = open_meteo_get(url, params=params, timeout=(5, 20)) if resp.status_code == 200: data = resp.json() # Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri) @@ -229,7 +230,7 @@ def get_weather_multi_model(lat, lon, short_term_models, long_term_models, forec "timezone": timezone if timezone else TZ_STR, "models": model, "forecast_days": forecast_days } try: - resp = requests.get(url, params=params, timeout=25) + resp = open_meteo_get(url, params=params, timeout=(5, 25)) if resp.status_code == 200: data = resp.json() # Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri) diff --git a/services/telegram-bot/road_weather.py b/services/telegram-bot/road_weather.py index bf53bb7..9c3033f 100644 --- a/services/telegram-bot/road_weather.py +++ b/services/telegram-bot/road_weather.py @@ -14,6 +14,7 @@ import requests import time from logging.handlers import RotatingFileHandler from typing import Dict, List, Tuple, Optional +from open_meteo_client import open_meteo_get # Setup logging SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -361,7 +362,7 @@ def get_coordinates_from_city(city_name: str) -> Optional[Tuple[float, float, st url = "https://geocoding-api.open-meteo.com/v1/search" params = {"name": city_name, "count": 1, "language": "it"} try: - resp = requests.get(url, params=params, timeout=5) + resp = open_meteo_get(url, params=params, timeout=(5, 10)) if resp.status_code == 200: data = resp.json() if data.get("results"): @@ -449,7 +450,7 @@ def get_weather_data(lat: float, lon: float, model_slug: str) -> Optional[Dict]: } try: - resp = requests.get(url, params=params, timeout=10) + resp = open_meteo_get(url, params=params, timeout=(5, 10)) if resp.status_code == 200: data = resp.json() # Verifica che snowfall sia presente nei dati diff --git a/services/telegram-bot/severe_weather.py b/services/telegram-bot/severe_weather.py index 40aed9b..dc1a856 100644 --- a/services/telegram-bot/severe_weather.py +++ b/services/telegram-bot/severe_weather.py @@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import open_meteo_get # ============================================================================= # SEVERE WEATHER ALERT (next 48h) - Casa (LAT/LON) @@ -302,7 +303,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional use_minutely = True try: - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + r = open_meteo_get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 25)) if r.status_code == 400: # Se 400 e abbiamo minutely_15, riprova senza if use_minutely and "minutely_15" in params: @@ -310,7 +311,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -329,7 +330,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -361,7 +362,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -375,7 +376,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = requests.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = open_meteo_get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: diff --git a/services/telegram-bot/severe_weather_circondario.py b/services/telegram-bot/severe_weather_circondario.py index 81d6861..4cdb41b 100755 --- a/services/telegram-bot/severe_weather_circondario.py +++ b/services/telegram-bot/severe_weather_circondario.py @@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import open_meteo_get # ============================================================================= # SEVERE WEATHER ALERT CIRCONDARIO (next 48h) - Analisi Temporali Severi @@ -255,7 +256,7 @@ def fetch_forecast(models_value: str, lat: float, lon: float) -> Optional[Dict]: params["hourly"] += ",cape" # ICON potrebbe avere CAPE try: - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + r = open_meteo_get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 25)) if r.status_code == 400: try: j = r.json() diff --git a/services/telegram-bot/smart_irrigation_advisor.py b/services/telegram-bot/smart_irrigation_advisor.py index b88ef11..fe0cf45 100755 --- a/services/telegram-bot/smart_irrigation_advisor.py +++ b/services/telegram-bot/smart_irrigation_advisor.py @@ -19,6 +19,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import open_meteo_get # ============================================================================= # CONFIGURATION @@ -278,7 +279,7 @@ def fetch_soil_and_weather(lat: float, lon: float, timezone: str = TZ) -> Option } try: - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=30) + r = open_meteo_get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 30)) if r.status_code == 400: try: j = r.json() @@ -331,7 +332,7 @@ def fetch_weather_only(lat: float, lon: float, timezone: str = TZ) -> Optional[D } try: - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=30) + r = open_meteo_get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 30)) r.raise_for_status() return r.json() except Exception as e: @@ -1495,6 +1496,12 @@ def main(): lon = args.lon if args.lon is not None else DEFAULT_LON location = args.location if args.location else DEFAULT_LOCATION_NAME timezone = args.timezone if args.timezone else TZ + + run_mode = "auto" if args.auto else "manual" + LOGGER.info("Heartbeat: start mode=%s location=%s", run_mode, location) + if args.auto: + now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"{now_str} INFO Heartbeat auto run for {location}") # Determina modalità operativa force_send = args.force or args.debug diff --git a/services/telegram-bot/snow_radar.py b/services/telegram-bot/snow_radar.py index 1b4dc88..1585731 100755 --- a/services/telegram-bot/snow_radar.py +++ b/services/telegram-bot/snow_radar.py @@ -13,6 +13,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import configure_open_meteo_session # ============================================================================= # snow_radar.py @@ -199,7 +200,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float) -> Optional[ } try: - r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 25)) if r.status_code == 400: try: j = r.json() @@ -642,6 +643,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id # Analizza località predefinite LOGGER.info("Analisi %d località predefinite...", len(LOCATIONS)) with requests.Session() as session: + configure_open_meteo_session(session, headers=HTTP_HEADERS) results = [] for i, loc in enumerate(LOCATIONS): # Calcola distanza da San Marino diff --git a/services/telegram-bot/student_alert.py b/services/telegram-bot/student_alert.py index 693a96e..e96e1b0 100644 --- a/services/telegram-bot/student_alert.py +++ b/services/telegram-bot/student_alert.py @@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo import requests from dateutil import parser +from open_meteo_client import configure_open_meteo_session # ============================================================================= # student_alert.py @@ -222,7 +223,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str) if model == MODEL_AROME: params["minutely_15"] = "precipitation,rain,snowfall,precipitation_probability,temperature_2m" try: - r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=(5, 25)) if r.status_code == 400: # Se 400 e abbiamo minutely_15, riprova senza if "minutely_15" in params and model == MODEL_AROME: @@ -230,7 +231,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -243,7 +244,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -275,7 +276,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -289,7 +290,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -303,7 +304,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str) params_no_minutely = params.copy() del params_no_minutely["minutely_15"] try: - r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=25) + r2 = session.get(OPEN_METEO_URL, params=params_no_minutely, headers=HTTP_HEADERS, timeout=(5, 25)) if r2.status_code == 200: return r2.json() except Exception: @@ -580,6 +581,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None comparisons: Dict[str, Dict] = {} # point_name -> comparison info with requests.Session() as session: + configure_open_meteo_session(session, headers=HTTP_HEADERS) # Trigger: Bologna bo = POINTS[0] bo_data_arome = get_forecast(session, bo["lat"], bo["lon"], MODEL_AROME)